def default_get(self, fields):
        res = super(MrpProductProduce, self).default_get(fields)
        if self._context and self._context.get('active_id'):
            production = self.env['mrp.production'].browse(self._context['active_id'])
            serial_finished = (production.product_id.tracking == 'serial')
            if serial_finished:
                todo_quantity = 1.0
            else:
                main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id)
                todo_quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done'))
                todo_quantity = todo_quantity if (todo_quantity > 0) else 0
            if 'production_id' in fields:
                res['production_id'] = production.id
            if 'product_id' in fields:
                res['product_id'] = production.product_id.id
            if 'product_uom_id' in fields:
                res['product_uom_id'] = production.product_uom_id.id
            if 'serial' in fields:
                res['serial'] = bool(serial_finished)
            if 'product_qty' in fields:
                res['product_qty'] = todo_quantity
            if 'produce_line_ids' in fields:
                lines = []
                for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel')):
                    qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty
                    for move_line in move.move_line_ids:
                        if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) < 0:
                            break
                        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': 0.0,
                            'lot_id': move_line.lot_id.id,
                            'product_uom_id': move.product_uom.id,
                            'product_id': move.product_id.id,
                        })
                        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': 0.0,
                                    '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': 0.0,
                                'product_uom_id': move.product_uom.id,
                                'product_id': move.product_id.id,
                            })

                res['produce_line_ids'] = [(0, 0, x) for x in lines]
        return res
Example #2
0
 def write(self, values):
     lines = False
     changed_lines = False
     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 float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1)
         changed_lines = self.filtered(
             lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0)
         if changed_lines:
             orders = self.mapped('order_id')
             for order in orders:
                 order_lines = changed_lines.filtered(lambda x: x.order_id == order)
                 msg = ""
                 if any([values['product_uom_qty'] < x.product_uom_qty for x in order_lines]):
                     msg += "<b>" + _('The ordered quantity has been decreased. Do not forget to take it into account on your invoices and delivery orders.') + '</b>'
                 msg += "<ul>"
                 for line in order_lines:
                     msg += "<li> %s:" % (line.product_id.display_name,)
                     msg += "<br/>" + _("Ordered Quantity") + ": %s -> %s <br/>" % (line.product_uom_qty, float(values['product_uom_qty']),)
                     if line.product_id.type in ('consu', 'product'):
                         msg += _("Delivered Quantity") + ": %s <br/>" % (line.qty_delivered,)
                     msg += _("Invoiced Quantity") + ": %s <br/>" % (line.qty_invoiced,)
                 msg += "</ul>"
                 order.message_post(body=msg)
     result = super(SaleOrderLine, self).write(values)
     if lines:
         lines._action_procurement_create()
     return result
Example #3
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') if not order_line.is_expense}

        if values.get('partner_shipping_id'):
            new_partner = self.env['res.partner'].browse(values.get('partner_shipping_id'))
            for record in self:
                picking = record.mapped('picking_ids').filtered(lambda x: x.state not in ('done', 'cancel'))
                addresses = (record.partner_shipping_id.display_name, new_partner.display_name)
                message = _("""The delivery address has been changed on the Sales Order<br/>
                        From <strong>"%s"</strong> To <strong>"%s"</strong>,
                        You should probably update the partner on this document.""") % addresses
                picking.activity_schedule('mail.mail_activity_data_warning', note=message, user_id=self.env.user.id)

        res = super(SaleOrder, self).write(values)
        if values.get('order_line') and self.state == 'sale':
            for order in self:
                to_log = {}
                order_lines_to_run = self.env['sale.order.line']
                for order_line in order.order_line:
                    if order_line not in pre_order_line_qty:
                        order_lines_to_run |= order_line
                    elif 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])
                    elif float_compare(order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) > 0:
                        order_lines_to_run |= 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)
                if order_lines_to_run:
                    order_lines_to_run._action_launch_stock_rule(pre_order_line_qty)
        return res
Example #4
0
 def _check_availability_warning(self, product_id, product_qty, ignore_warehouse=False):
     if product_id.type == 'product':
         precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
         product_by_wh = 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'
         )
         if float_compare(product_by_wh.virtual_available, product_qty, precision_digits=precision) == -1:
             is_available = self._check_routing(product_id)
             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, product_id.name,
                            product_by_wh.virtual_available,
                            product_by_wh.uom_id.name, self.order_id.warehouse_id.name)
                 # We check if some products are available in other warehouses.
                 if not ignore_warehouse and float_compare(product_by_wh.virtual_available, product_id.virtual_available,
                                  precision_digits=precision) == -1:
                     message += _('\nThere are %s %s available across all warehouses.\n\n') % \
                                (product_id.virtual_available, product_by_wh.uom_id.name)
                     for warehouse in self.env['stock.warehouse'].search([]):
                         quantity = product_id.with_context(warehouse=warehouse.id).virtual_available
                         if quantity > 0:
                             message += "%s: %s %s\n" % (warehouse.name, quantity, product_id.uom_id.name)
                 warning_mess = {
                     'title': _('Not enough inventory!'),
                     'message': message
                 }
                 return {'warning': warning_mess}
     return {}
Example #5
0
 def move_validate(self):
     """ Validate moves based on a production order. """
     moves = self._filter_closed_moves()
     quant_obj = self.env["stock.quant"]
     moves_todo = self.env["stock.move"]
     moves_to_unreserve = self.env["stock.move"]
     # Create extra moves where necessary
     for move in moves:
         rounding = move.product_uom.rounding
         if float_compare(move.quantity_done, 0.0, precision_rounding=rounding) <= 0:
             continue
         moves_todo |= move
         moves_todo |= move._create_extra_move()
     # Split moves where necessary and move quants
     for move in moves_todo:
         rounding = move.product_uom.rounding
         if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=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
             )
             new_move = move.split(qty_split)
             # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
             move.move_lot_ids.filtered(lambda x: not x.done_wo or x.quantity_done == 0.0).write(
                 {"move_id": new_move}
             )
             self.browse(new_move).quantity_done = 0.0
         main_domain = [("qty", ">", 0)]
         preferred_domain = [("reservation_id", "=", move.id)]
         fallback_domain = [("reservation_id", "=", False)]
         fallback_domain2 = ["&", ("reservation_id", "!=", move.id), ("reservation_id", "!=", False)]
         preferred_domain_list = [preferred_domain] + [fallback_domain] + [fallback_domain2]
         if move.has_tracking == "none":
             quants = quant_obj.quants_get_preferred_domain(
                 move.product_qty, move, domain=main_domain, preferred_domain_list=preferred_domain_list
             )
             self.env["stock.quant"].quants_move(quants, move, move.location_dest_id)
         else:
             for movelot in move.move_lot_ids:
                 if float_compare(movelot.quantity_done, 0, precision_rounding=rounding) > 0:
                     if not movelot.lot_id:
                         raise UserError(_("You need to supply a lot/serial number."))
                     qty = move.product_uom._compute_quantity(movelot.quantity_done, move.product_id.uom_id)
                     quants = quant_obj.quants_get_preferred_domain(
                         qty,
                         move,
                         lot_id=movelot.lot_id.id,
                         domain=main_domain,
                         preferred_domain_list=preferred_domain_list,
                     )
                     self.env["stock.quant"].quants_move(
                         quants, move, move.location_dest_id, lot_id=movelot.lot_id.id
                     )
         moves_to_unreserve |= move
         # Next move in production order
         if move.move_dest_id:
             move.move_dest_id.action_assign()
     moves_to_unreserve.quants_unreserve()
     moves_todo.write({"state": "done", "date": fields.Datetime.now()})
     return moves_todo
Example #6
0
 def _prepare_move(self, line):
     category_id = line.asset_id.category_id
     depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self)
     company_currency = line.asset_id.company_id.currency_id
     current_currency = line.asset_id.currency_id
     prec = company_currency.decimal_places
     amount = current_currency.with_context(date=depreciation_date).compute(line.amount, company_currency)
     asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids))
     move_line_1 = {
         'name': asset_name,
         'account_id': category_id.account_depreciation_id.id,
         'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
         'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
         'partner_id': line.asset_id.partner_id.id,
         'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False,
         'currency_id': company_currency != current_currency and current_currency.id or False,
         'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0,
     }
     move_line_2 = {
         'name': asset_name,
         'account_id': category_id.account_depreciation_expense_id.id,
         'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
         'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
         'partner_id': line.asset_id.partner_id.id,
         'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False,
         'currency_id': company_currency != current_currency and current_currency.id or False,
         'amount_currency': company_currency != current_currency and line.amount or 0.0,
     }
     move_vals = {
         'ref': line.asset_id.code,
         'date': depreciation_date or False,
         'journal_id': category_id.journal_id.id,
         'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
     }
     return move_vals
 def money_transfer_draft(self):
     '''转账单的反审核按钮,外币要考虑是转入还是转出'''
     self.ensure_one()
     decimal_amount = self.env.ref('core.decimal_amount')
     for line in self.line_ids:
         if line.currency_amount > 0:
             if line.in_bank_id.currency_id: # 如果填充了转入账户的币别,则说明转入账户为外币
                 if float_compare(line.in_bank_id.balance, line.currency_amount, precision_digits=decimal_amount.digits) == -1:
                     raise UserError('转入账户余额不足。\n转入账户余额:%s 本次转出外币金额:%s'
                             % (line.in_bank_id.balance, line.currency_amount))
                 else:   # 转入账户余额充足
                     line.in_bank_id.balance -= line.currency_amount
                     line.out_bank_id.balance += line.amount
             else:   # 转入账户为本位币
                 if float_compare(line.in_bank_id.balance, line.amount, precision_digits=decimal_amount.digits) == -1:
                     raise UserError('转入账户余额不足。\n转入账户余额:%s 本次转出金额:%s'
                             % (line.in_bank_id.balance, line.amount))
                 else:
                     line.in_bank_id.balance -= line.amount
                     line.out_bank_id.balance += line.currency_amount
         else:   # 转入/转出账户都为本位币
             if float_compare(line.in_bank_id.balance, line.amount, precision_digits=decimal_amount.digits) == -1:
                 raise UserError('转入账户余额不足。\n转入账户余额:%s 本次转出金额:%s'
                             % (line.in_bank_id.balance, line.amount))
             else:
                 line.in_bank_id.balance -= line.amount
                 line.out_bank_id.balance += line.amount
     self.state = 'draft'
     return True
 def _generate_lines_values(self, move, qty_to_consume):
     """ Create workorder line. First generate line based on the reservation,
     in order to match the reservation. If the quantity to consume is greater
     than the reservation quantity then create line with the correct quantity
     to consume but without lot or serial number.
     """
     lines = []
     is_tracked = move.product_id.tracking != 'none'
     for move_line in move.move_line_ids:
         if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0:
             break
         # move line already 'used' in workorder (from its lot for instance)
         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
         # search wo line on which the lot is not fully consumed or other reserved lot
         linked_wo_line = self.workorder_line_ids.filtered(
             lambda line: line.product_id == move_line.product_id and
             line.lot_id == move_line.lot_id
         )
         if linked_wo_line:
             if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0:
                 to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume')))
             else:
                 continue
         else:
             to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done)
         line = {
             'move_id': move.id,
             'product_id': move.product_id.id,
             'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id,
             'qty_to_consume': to_consume_in_line,
             'qty_reserved': to_consume_in_line,
             'lot_id': move_line.lot_id.id,
             'qty_done': to_consume_in_line
         }
         lines.append(line)
         qty_to_consume -= to_consume_in_line
     # The move has not reserved the whole quantity so we create new wo lines
     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:
                 line = {
                     'move_id': move.id,
                     'product_id': move.product_id.id,
                     'product_uom_id': move.product_id.uom_id.id,
                     'qty_to_consume': 1,
                     'qty_done': 1,
                 }
                 lines.append(line)
                 qty_to_consume -= 1
         else:
             line = {
                 'move_id': move.id,
                 'product_id': move.product_id.id,
                 'product_uom_id': move.product_uom.id,
                 'qty_to_consume': qty_to_consume,
                 'qty_done': qty_to_consume
             }
             lines.append(line)
     return lines
Example #9
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)
         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 but you only have %s %s available in %s warehouse.') % \
                         (self.product_uom_qty, self.product_uom.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 {}
Example #10
0
 def _compute_invoice_status(self):
     """
     Compute the invoice status of a SO line. Possible statuses:
     - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
       invoice. This is also hte default value if the conditions of no other status is met.
     - to invoice: we refer to the quantity to invoice of the line. Refer to method
       `_get_to_invoice_qty()` for more information on how this quantity is calculated.
     - upselling: this is possible only for a product invoiced on ordered quantities for which
       we delivered more than expected. The could arise if, for example, a project took more
       time than expected but we decided not to invoice the extra cost to the client. This
       occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity
       is removed from the list.
     - invoiced: the quantity invoiced is larger or equal to the quantity ordered.
     """
     precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
     for line in self:
         if line.state not in ('sale', 'done'):
             line.invoice_status = 'no'
         elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
             line.invoice_status = 'to invoice'
         elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
                 float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
             line.invoice_status = 'upselling'
         elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
             line.invoice_status = 'invoiced'
         else:
             line.invoice_status = 'no'
Example #11
0
File: stock.py Project: befks/odoo
 def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False):
     quant = super(StockQuant, self)._quant_create_from_move(qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to)
     quant._account_entry_move(move)
     if move.product_id.valuation == 'real_time':
         # If the precision required for the variable quant cost is larger than the accounting
         # precision, inconsistencies between the stock valuation and the accounting entries
         # may arise.
         # For example, a box of 13 units is bought 15.00. If the products leave the
         # stock one unit at a time, the amount related to the cost will correspond to
         # round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
         # record the difference on the new quant.
         # We need to make sure to able to extract at least one unit of the product. There is
         # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
         # unit and adapt the cost.
         curr_rounding = move.company_id.currency_id.rounding
         cost_rounded = float_round(quant.cost, precision_rounding=curr_rounding)
         cost_correct = cost_rounded
         if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
                 and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\
                 and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0:
             quant_correct = quant._quant_split(quant.qty - 1.0)
             cost_correct += (quant.qty * quant.cost) - (quant.qty * cost_rounded)
             quant.sudo().write({'cost': cost_rounded})
             quant_correct.sudo().write({'cost': cost_correct})
     return quant
Example #12
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'
Example #13
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(new_quantity_done, ml.product_uom_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) < 0:
                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)
Example #14
0
 def _check_holidays(self):
     for holiday in self:
         if holiday.holiday_type != 'employee' or holiday.type != 'remove' or not holiday.employee_id or holiday.holiday_status_id.limit:
             continue
         leave_days = holiday.holiday_status_id.get_days(holiday.employee_id.id)[holiday.holiday_status_id.id]
         if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \
           float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1:
             raise ValidationError(_('The number of remaining leaves is not sufficient for this leave type.\n'
                                     'Please verify also the leaves waiting for validation.'))
Example #15
0
 def _check_holidays(self):
     for holiday in self:
         if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.allocation_type == 'no':
             continue
         leave_days = holiday.holiday_status_id.get_days(holiday.employee_id.id)[holiday.holiday_status_id.id]
         if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \
           float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1:
             raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n'
                                     'Please also check the time off waiting for validation.'))
Example #16
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)
Example #17
0
    def record_production(self):
        if not self:
            return True

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

        # If last work order, then post lots used
        if not self.next_work_order_id:
            self._update_finished_move()

        # Transfer quantities from temporary to final move line or make them final
        self._update_moves()

        # Transfer lot (if present) and quantity produced to a finished workorder line
        if self.product_tracking != 'none':
            self._create_or_update_finished_line()

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

        # Suggest a finished lot on the next workorder
        if self.next_work_order_id and self.production_id.product_id.tracking != 'none' and not self.next_work_order_id.finished_lot_id:
            self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids)
            # As we may have changed the quantity to produce on the next workorder,
            # make sure to update its wokorder lines
            self.next_work_order_id._apply_update_workorder_lines()

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

        # Test if the production is done
        rounding = self.production_id.product_uom_id.rounding
        if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0:
            previous_wo = self.env['mrp.workorder']
            if self.product_tracking != 'none':
                previous_wo = self.env['mrp.workorder'].search([
                    ('next_work_order_id', '=', self.id)
                ])
            candidate_found_in_previous_wo = False
            if previous_wo:
                candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids)
            if not candidate_found_in_previous_wo:
                # self is the first workorder
                self.qty_producing = self.qty_remaining
                self.finished_lot_id = False
                if self.product_tracking == 'serial':
                    self.qty_producing = 1

            self._apply_update_workorder_lines()
        else:
            self.qty_producing = 0
            self.button_finish()
        return True
Example #18
0
    def _wrong_receipt_done(self):
        if self.state == 'done':
            raise UserError(u'请不要重复审核!')
        batch_one_list_wh = []
        batch_one_list = []
        for line in self.line_in_ids:
            if line.amount < 0:
                raise UserError(u'购货金额不能小于 0!请修改。')
            if line.goods_id.force_batch_one:
                wh_move_lines = self.env['wh.move.line'].search(
                    [('state', '=', 'done'), ('type', '=', 'in'), ('goods_id', '=', line.goods_id.id)])
                for move_line in wh_move_lines:
                    if (move_line.goods_id.id, move_line.lot) not in batch_one_list_wh and move_line.lot:
                        batch_one_list_wh.append(
                            (move_line.goods_id.id, move_line.lot))

            if (line.goods_id.id, line.lot) in batch_one_list_wh:
                raise UserError(u'仓库已存在相同序列号的商品!\n商品:%s 序列号:%s' %
                                (line.goods_id.name, line.lot))

        for line in self.line_in_ids:
            if line.goods_qty <= 0 or line.price_taxed < 0:
                raise UserError(u'商品 %s 的数量和含税单价不能小于0!' % line.goods_id.name)
            if line.goods_id.force_batch_one:
                if line.goods_qty > 1:
                    raise UserError(u'商品 %s 进行了序列号管理,数量必须为1' %
                                    line.goods_id.name)
                batch_one_list.append((line.goods_id.id, line.lot))

        if len(batch_one_list) > len(set(batch_one_list)):
            raise UserError(u'不能创建相同序列号的商品!\n 序列号列表为%s' %
                            [lot[1] for lot in batch_one_list])

        for line in self.line_out_ids:
            if line.amount < 0:
                raise UserError(u'退货金额不能小于 0!请修改。')
            if line.goods_qty <= 0 or line.price_taxed < 0:
                raise UserError(u'商品 %s 的数量和含税单价不能小于0!' % line.goods_id.name)

        if not self.bank_account_id and self.payment:
            raise UserError(u'付款额不为空时,请选择结算账户!')
        decimal_amount = self.env.ref('core.decimal_amount')
        if float_compare(self.payment, self.amount, precision_digits=decimal_amount.digits) == 1:
            raise UserError(u'本次付款金额不能大于折后金额!\n付款金额:%s 折后金额:%s' %
                            (self.payment, self.amount))
        if float_compare(sum(cost_line.amount for cost_line in self.cost_line_ids),
                         sum(line.share_cost for line in self.line_in_ids),
                         precision_digits=decimal_amount.digits) != 0:
            raise UserError(u'采购费用还未分摊或分摊不正确!\n采购费用:%s 分摊总费用:%s' %
                            (sum(cost_line.amount for cost_line in self.cost_line_ids),
                             sum(line.share_cost for line in self.line_in_ids)))
        return
Example #19
0
 def move_validate(self):
     ''' Validate moves based on a production order. '''
     moves = self._filter_closed_moves()
     quant_obj = self.env['stock.quant']
     moves_todo = self.env['stock.move']
     moves_to_unreserve = self.env['stock.move']
     # Create extra moves where necessary
     for move in moves:
         # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
         # it is possible that the user changed the value before posting the inventory by a value that should be
         # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
         # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be
         # consumed/produced and the moves are split correctly).
         rounding = move.product_uom.rounding
         move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
         if move.quantity_done <= 0:
             continue
         moves_todo |= move
         moves_todo |= move._create_extra_move()
     # Split moves where necessary and move quants
     for move in moves_todo:
         rounding = move.product_uom.rounding
         if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=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)
             new_move = move.split(qty_split)
             # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
             move.move_lot_ids.filtered(lambda x: not x.done_wo or x.quantity_done == 0.0).write({'move_id': new_move})
             self.browse(new_move).quantity_done = 0.0
         main_domain = [('qty', '>', 0)]
         preferred_domain = [('reservation_id', '=', move.id)]
         fallback_domain = [('reservation_id', '=', False)]
         fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
         preferred_domain_list = [preferred_domain] + [fallback_domain] + [fallback_domain2]
         if move.has_tracking == 'none':
             quants = quant_obj.quants_get_preferred_domain(move.product_qty, move, domain=main_domain, preferred_domain_list=preferred_domain_list)
             self.env['stock.quant'].quants_move(quants, move, move.location_dest_id)
         else:
             for movelot in move.move_lot_ids:
                 if float_compare(movelot.quantity_done, 0, precision_rounding=rounding) > 0:
                     if not movelot.lot_id:
                         raise UserError(_('You need to supply a lot/serial number.'))
                     qty = move.product_uom._compute_quantity(movelot.quantity_done, move.product_id.uom_id)
                     quants = quant_obj.quants_get_preferred_domain(qty, move, lot_id=movelot.lot_id.id, domain=main_domain, preferred_domain_list=preferred_domain_list)
                     self.env['stock.quant'].quants_move(quants, move, move.location_dest_id, lot_id = movelot.lot_id.id)
         moves_to_unreserve |= move
         # Next move in production order
         if move.move_dest_id:
             move.move_dest_id.action_assign()
     moves_to_unreserve.quants_unreserve()
     moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
     return moves_todo
Example #20
0
    def create_move(self, post_move=True):
        created_moves = self.env["account.move"]
        for line in self:
            category_id = line.asset_id.category_id
            depreciation_date = (
                self.env.context.get("depreciation_date") or line.depreciation_date or fields.Date.context_today(self)
            )
            company_currency = line.asset_id.company_id.currency_id
            current_currency = line.asset_id.currency_id
            amount = current_currency.compute(line.amount, company_currency)
            sign = (category_id.journal_id.type == "purchase" or category_id.journal_id.type == "sale" and 1) or -1
            asset_name = line.asset_id.name + " (%s/%s)" % (line.sequence, len(line.asset_id.depreciation_line_ids))
            prec = self.env["decimal.precision"].precision_get("Account")
            move_line_1 = {
                "name": asset_name,
                "account_id": category_id.account_depreciation_id.id,
                "debit": 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
                "credit": amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
                "journal_id": category_id.journal_id.id,
                "partner_id": line.asset_id.partner_id.id,
                "analytic_account_id": category_id.account_analytic_id.id if category_id.type == "sale" else False,
                "currency_id": company_currency != current_currency and current_currency.id or False,
                "amount_currency": company_currency != current_currency and -sign * line.amount or 0.0,
            }
            move_line_2 = {
                "name": asset_name,
                "account_id": category_id.account_depreciation_expense_id.id,
                "credit": 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount,
                "debit": amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
                "journal_id": category_id.journal_id.id,
                "partner_id": line.asset_id.partner_id.id,
                "analytic_account_id": category_id.account_analytic_id.id if category_id.type == "purchase" else False,
                "currency_id": company_currency != current_currency and current_currency.id or False,
                "amount_currency": company_currency != current_currency and sign * line.amount or 0.0,
            }
            move_vals = {
                "ref": line.asset_id.code,
                "date": depreciation_date or False,
                "journal_id": category_id.journal_id.id,
                "line_ids": [(0, 0, move_line_1), (0, 0, move_line_2)],
            }
            move = self.env["account.move"].create(move_vals)
            line.write({"move_id": move.id, "move_check": True})
            created_moves |= move

        if post_move and created_moves:
            created_moves.filtered(
                lambda m: any(m.asset_depreciation_ids.mapped("asset_id.category_id.open_asset"))
            ).post()
        return [x.id for x in created_moves]
    def _create_extra_move_lines(self):
        """Create new sml if quantity produced is bigger than the reserved one"""
        vals_list = []
        quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False)
        # Search for a sub-locations where the product is available.
        # Loop on the quants to get the locations. If there is not enough
        # quantity into stock, we take the move location. Anyway, no
        # reservation is made, so it is still possible to change it afterwards.
        for quant in quants:
            quantity = quant.quantity - quant.reserved_quantity
            rounding = quant.product_uom_id.rounding
            if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or
                    float_compare(quantity, 0, precision_rounding=rounding) <= 0):
                continue
            vals = {
                'move_id': self.move_id.id,
                'product_id': self.product_id.id,
                'location_id': quant.location_id.id,
                'location_dest_id': self.move_id.location_dest_id.id,
                'product_uom_qty': 0,
                'product_uom_id': quant.product_uom_id.id,
                'qty_done': min(quantity, self.qty_done),
                'lot_produced_id': self._get_final_lot().id,
            }
            if self.lot_id:
                vals.update({'lot_id': self.lot_id.id})

            vals_list.append(vals)
            self.qty_done -= vals['qty_done']
            # If all the qty_done is distributed, we can close the loop
            if float_compare(self.qty_done, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
                break

        if float_compare(self.qty_done, 0, precision_rounding=self.product_uom_id.rounding) > 0:
            vals = {
                'move_id': self.move_id.id,
                'product_id': self.product_id.id,
                'location_id': self.move_id.location_id.id,
                'location_dest_id': self.move_id.location_dest_id.id,
                'product_uom_qty': 0,
                'product_uom_id': self.product_uom_id.id,
                'qty_done': self.qty_done,
                'lot_produced_id': self._get_final_lot().id,
            }
            if self.lot_id:
                vals.update({'lot_id': self.lot_id.id})

            vals_list.append(vals)

        return vals_list
Example #22
0
    def _action_procurement_create(self):
        """
        Create procurements based on quantity ordered. If the quantity is increased, new
        procurements are created. If the quantity is decreased, no automated action is taken.
        """
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
        new_procs = self.env['procurement.order']  # Empty recordset
        for line in self:
            if line.state != 'sale' or not line.product_id._need_procurement():
                continue
            qty = 0.0
            for proc in line.procurement_ids:
                qty += proc.product_qty
            if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
                continue

            if not line.order_id.procurement_group_id:
                vals = line.order_id._prepare_procurement_group()
                line.order_id.procurement_group_id = self.env["procurement.group"].create(vals)

            vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id)
            vals['product_qty'] = line.product_uom_qty - qty
            new_proc = self.env["procurement.order"].create(vals)
            new_proc.message_post_with_view('mail.message_origin_link',
                values={'self': new_proc, 'origin': line.order_id},
                subtype_id=self.env.ref('mail.mt_note').id)
            new_procs += new_proc
        new_procs.run()
        return new_procs
Example #23
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)
         )
Example #24
0
 def do_produce(self):
     # Nothing to do for lots since values are created using default data (stock.move.lots)
     moves = self.production_id.move_raw_ids
     quantity = self.product_qty
     if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
         raise UserError(_('You should at least produce some quantity'))
     for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')):
         if move.unit_factor:
             rounding = move.product_uom.rounding
             move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding)
     moves = self.production_id.move_finished_ids.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel'))
     for move in moves:
         rounding = move.product_uom.rounding
         if move.product_id.id == self.production_id.product_id.id:
             move.quantity_done_store += float_round(quantity, precision_rounding=rounding)
         elif move.unit_factor:
             # byproducts handling
             move.quantity_done_store += 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'}
Example #25
0
    def _action_launch_procurement_rule(self):
        """
        Launch procurement group run method with required/custom fields genrated by a
        sale order line. procurement group will launch '_run_move', '_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 = 0.0
            for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
                qty += move.product_qty
            if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
                continue

            if not line.order_id.procurement_group_id:
                line.order_id.procurement_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,
                })
            values = line._prepare_procurement_values(group_id=line.order_id.procurement_group_id)
            product_qty = line.product_uom_qty - qty
            try:
                self.env['procurement.group'].run(line.product_id, product_qty, line.product_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
Example #26
0
    def _check_sum(self):
        """ Check if each cost line its valuation lines sum to the correct amount
        and if the overall total amount is correct also """
        prec_digits = self.env['decimal.precision'].precision_get('Account')
        for landed_cost in self:
            total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
            if not tools.float_compare(total_amount, landed_cost.amount_total, precision_digits=prec_digits) == 0:
                return False

            val_to_cost_lines = defaultdict(lambda: 0.0)
            for val_line in landed_cost.valuation_adjustment_lines:
                val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
            if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0
                   for cost_line, val_amount in val_to_cost_lines.iteritems()):
                return False
        return True
Example #27
0
    def _get_bom_delivered(self, bom=False):
        self.ensure_one()
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')

        # In the case of a kit, we need to check if all components are received or not.
        # nothing policy. A product can have several BoMs, we don't know which one was used when the
        # receipt was created.
        bom_delivered = {}
        if bom:
            bom_delivered[bom.id] = False
            product_uom_qty_bom = self.env['product.uom']._compute_qty_obj(self.product_uom, self.product_qty, bom.product_uom_id)
            boms, lines = bom.explode(self.product_id, product_uom_qty_bom)
            for bom_line, data in lines:
                qty = 0.0
                for move in self.move_ids.filtered(lambda x: x.state == 'done' and x.product_id == bom_line.product_id):
                    qty += self.env['product.uom']._compute_qty(move.product_uom.id, move.product_uom_qty, bom_line.product_uom_id.id)
                if float_compare(qty, data['qty'], precision_digits=precision) < 0:
                    bom_delivered[bom.id] = False
                    break
                else:
                    bom_delivered[bom.id] = True
        if bom_delivered and any(bom_delivered.values()):
            return self.product_qty
        elif bom_delivered:
            return 0.0
Example #28
0
    def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False):
        self.ensure_one()
        if date is None:
            date = fields.Date.context_today(self)
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')

        res = self.env['product.supplierinfo']
        for seller in self._prepare_sellers(params):
            # Set quantity in UoM of seller
            quantity_uom_seller = quantity
            if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
                quantity_uom_seller = uom_id._compute_quantity(quantity_uom_seller, seller.product_uom)

            if seller.date_start and seller.date_start > date:
                continue
            if seller.date_end and seller.date_end < date:
                continue
            if partner_id and seller.name not in [partner_id, partner_id.parent_id]:
                continue
            if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1:
                continue
            if seller.product_id and seller.product_id != self:
                continue

            res |= seller
            break
        return res
Example #29
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'
         }
Example #30
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_qty = self.env["product.uom"]._compute_qty_obj(
             self.product_uom, self.product_uom_qty, self.product_id.uom_id
         )
         if float_compare(self.product_id.virtual_available, product_qty, precision_digits=precision) == -1:
             is_available = self._check_routing()
             if not is_available:
                 warning_mess = {
                     "title": _("Not enough inventory!"),
                     "message": _(
                         "You plan to sell %.2f %s but you only have %.2f %s available!\nThe stock on hand is %.2f %s."
                     )
                     % (
                         self.product_uom_qty,
                         self.product_uom.name,
                         self.product_id.virtual_available,
                         self.product_id.uom_id.name,
                         self.product_id.qty_available,
                         self.product_id.uom_id.name,
                     ),
                 }
                 return {"warning": warning_mess}
     return {}
Example #31
0
    def button_validate(self):
        if any(cost.state != 'draft' for cost in self):
            raise UserError(_('Only draft landed costs can be validated'))
        if any(not cost.valuation_adjustment_lines for cost in self):
            raise UserError(_('No valuation adjustments lines. You should maybe recompute the landed costs.'))
        if not self._check_sum():
            raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))

        for cost in self:
            move = self.env['account.move'].create({
                'journal_id': cost.account_journal_id.id,
                'date': cost.date,
                'ref': cost.name
            })
            for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
                per_unit = line.final_cost / line.quantity
                diff = per_unit - line.former_cost_per_unit

                # If the precision required for the variable diff is larger than the accounting
                # precision, inconsistencies between the stock valuation and the accounting entries
                # may arise.
                # For example, a landed cost of 15 divided in 13 units. If the products leave the
                # stock one unit at a time, the amount related to the landed cost will correspond to
                # round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
                # record the difference on the new quant.
                # We need to make sure to able to extract at least one unit of the product. There is
                # an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
                # unit and adapt the cost.
                curr_rounding = line.move_id.company_id.currency_id.rounding
                diff_rounded = tools.float_round(diff, precision_rounding=curr_rounding)
                diff_correct = diff_rounded
                quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True)
                quant_correct = False
                if quants\
                        and tools.float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
                        and tools.float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\
                        and tools.float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0:
                    # Search for existing quant of quantity = 1.0 to avoid creating a new one
                    quant_correct = quants.filtered(lambda r: tools.float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0)
                    if not quant_correct:
                        quant_correct = quants[0]._quant_split(quants[0].qty - 1.0)
                    else:
                        quant_correct = quant_correct[0]
                        quants = quants - quant_correct
                    diff_correct += (line.quantity * diff) - (line.quantity * diff_rounded)
                    diff = diff_rounded

                quant_dict = {}
                for quant in quants:
                    quant_dict[quant] = quant.cost + diff
                if quant_correct:
                    quant_dict[quant_correct] = quant_correct.cost + diff_correct
                for quant, value in quant_dict.items():
                    quant.sudo().write({'cost': value})
                qty_out = 0
                for quant in line.move_id.quant_ids:
                    if quant.location_id.usage != 'internal':
                        qty_out += quant.qty
                line._create_accounting_entries(move, qty_out)
            cost.write({'state': 'done', 'account_move_id': move.id})
            move.post()
        return True
Example #32
0
 def _generate_lines_values(self, move, qty_to_consume):
     """ Create workorder line. First generate line based on the reservation,
     in order to prefill reserved quantity, lot and serial number.
     If the quantity to consume is greater than the reservation quantity then
     create line with the correct quantity to consume but without lot or
     serial number.
     """
     lines = []
     is_tracked = move.product_id.tracking == 'serial'
     if move in self.move_raw_ids._origin:
         # Get the inverse_name (many2one on line) of raw_workorder_line_ids
         initial_line_values = {
             self.raw_workorder_line_ids._get_raw_workorder_inverse_name():
             self.id
         }
     else:
         # Get the inverse_name (many2one on line) of finished_workorder_line_ids
         initial_line_values = {
             self.finished_workorder_line_ids._get_finished_workoder_inverse_name(
             ):
             self.id
         }
     for move_line in move.move_line_ids:
         line = dict(initial_line_values)
         if float_compare(
                 qty_to_consume,
                 0.0,
                 precision_rounding=move.product_uom.rounding) <= 0:
             break
         # move line already 'used' in workorder (from its lot for instance)
         if move_line.lot_produced_ids or float_compare(
                 move_line.product_uom_qty,
                 move_line.qty_done,
                 precision_rounding=move.product_uom.rounding) <= 0:
             continue
         # search wo line on which the lot is not fully consumed or other reserved lot
         linked_wo_line = self._workorder_line_ids().filtered(
             lambda line: line.move_id == move and line.lot_id == move_line.
             lot_id)
         if linked_wo_line:
             if float_compare(
                     sum(linked_wo_line.mapped('qty_to_consume')),
                     move_line.product_uom_qty - move_line.qty_done,
                     precision_rounding=move.product_uom.rounding) < 0:
                 to_consume_in_line = min(
                     qty_to_consume,
                     move_line.product_uom_qty - move_line.qty_done -
                     sum(linked_wo_line.mapped('qty_to_consume')))
             else:
                 continue
         else:
             to_consume_in_line = min(
                 qty_to_consume,
                 move_line.product_uom_qty - move_line.qty_done)
         line.update({
             'move_id':
             move.id,
             'product_id':
             move.product_id.id,
             'product_uom_id':
             is_tracked and move.product_id.uom_id.id
             or move.product_uom.id,
             'qty_to_consume':
             to_consume_in_line,
             'qty_reserved':
             to_consume_in_line,
             'lot_id':
             move_line.lot_id.id,
             'qty_done':
             to_consume_in_line,
         })
         lines.append(line)
         qty_to_consume -= to_consume_in_line
     # The move has not reserved the whole quantity so we create new wo lines
     if float_compare(qty_to_consume,
                      0.0,
                      precision_rounding=move.product_uom.rounding) > 0:
         line = dict(initial_line_values)
         if move.product_id.tracking == 'serial':
             while float_compare(
                     qty_to_consume,
                     0.0,
                     precision_rounding=move.product_uom.rounding) > 0:
                 line.update({
                     'move_id': move.id,
                     'product_id': move.product_id.id,
                     'product_uom_id': move.product_id.uom_id.id,
                     'qty_to_consume': 1,
                     'qty_done': 1,
                 })
                 lines.append(line)
                 qty_to_consume -= 1
         else:
             line.update({
                 'move_id': move.id,
                 'product_id': move.product_id.id,
                 'product_uom_id': move.product_uom.id,
                 'qty_to_consume': qty_to_consume,
                 'qty_done': qty_to_consume,
             })
             lines.append(line)
     return lines
Example #33
0
 def _compute_is_produced(self):
     self.is_produced = False
     for order in self.filtered(lambda p: p.production_id and p.production_id.product_uom_id):
         rounding = order.production_id.product_uom_id.rounding
         order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0
    def _action_launch_procurement_rule(self):
        """
        Launch procurement group run method with required/custom fields genrated by a
        sale order line. procurement group will launch '_run_move', '_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 = 0.0
            for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
                qty += move.product_uom._compute_quantity(
                    move.product_uom_qty,
                    line.product_uom,
                    rounding_method='HALF-UP')
            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
    def olive_oil_tank_check(self,
                             raise_if_not_merged=True,
                             raise_if_empty=True):
        '''Returns quantity
        Always raises when there are reservations
        '''
        self.ensure_one()
        sqo = self.env['stock.quant']
        ppo = self.env['product.product']
        prec = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        tank_type = self.olive_tank_type
        tank_type_label = dict(
            self.fields_get(
                'olive_tank_type',
                'selection')['olive_tank_type']['selection'])[tank_type]
        # Tank configuration checks
        if not tank_type:
            raise UserError(
                _("The stock location '%s' is not an olive oil tank.") %
                self.display_name)
        if not self.oil_product_id:
            raise UserError(_("Missing oil product on tank '%s'.") % self.name)
        if self.oil_product_id.olive_type != 'oil':
            raise UserError(
                _("Oil product '%s' configured on tank '%s' is not "
                  "an olive oil product.") %
                (self.oil_product_id.display_name, self.name))
        if tank_type != 'risouletto' and not self.olive_season_id:
            raise UserError(
                _("Olive season is not configured on tank '%s'.") % self.name)

        # raise if empty
        quant_qty_rg = sqo.read_group([('location_id', '=', self.id)], ['qty'],
                                      [])
        qty = quant_qty_rg and quant_qty_rg[0]['qty'] or 0
        fcompare = float_compare(qty, 0, precision_digits=prec)
        if fcompare < 0:
            raise UserError(
                _("The tank '%s' has a negative quantity (%s).") %
                (self.name, qty))
        elif fcompare == 0:
            if raise_if_empty:
                raise UserError(_("The tank '%s' is empty.") % self.name)
            return 0  # WARN : no further checks if empty

        # raise if there are reservations
        reserved_quants_count = sqo.search([('location_id', '=', self.id),
                                            ('reservation_id', '!=', False)],
                                           count=True)
        if reserved_quants_count:
            raise UserError(
                _("There are %d reserved quants in tank '%s'.") %
                (reserved_quants_count, self.name))

        if raise_if_not_merged:
            quant_lot_rg = sqo.read_group([('location_id', '=', self.id)],
                                          ['qty', 'lot_id'], ['lot_id'])
            if len(quant_lot_rg) > 1:
                raise UserError(
                    _("The tank '%s' (type '%s') is not merged: it "
                      "contains several different lots.") %
                    (self.name, tank_type_label))
            # for risouletto, there are additionnal checks for raise_if_not_merged
            # see below

        quant_product_rg = sqo.read_group([('location_id', '=', self.id)],
                                          ['qty', 'product_id'],
                                          ['product_id'])
        if tank_type == 'risouletto':
            for quant_product in quant_product_rg:
                product = ppo.browse(quant_product['product_id'][0])
                if raise_if_not_merged and product != self.oil_product_id:
                    raise UserError(
                        _("The tank '%s' (type '%s') contains '%s', "
                          "so it not merged.") %
                        (self.name, tank_type_label, product.display_name))
                if product.olive_type != 'oil':
                    raise UserError(
                        _("The tank '%s' (type '%s') contains '%s', "
                          "which is not an olive oil product.") %
                        (self.name, tank_type_label, product.display_name))
        else:  # regular oil => always 1 product, same as configured on tank
            if len(quant_product_rg) > 1:
                raise UserError(
                    _("There are several different products in tank '%s'. "
                      "This should never happen in an oil tank which is "
                      "not a risouletto tank.") % self.name)
            product = ppo.browse(quant_product_rg[0]['product_id'][0])
            if product != self.oil_product_id:
                raise UserError(
                    _("The tank '%s' (type '%s') contains '%s' but it is "
                      "configured to contain '%s'. This should never "
                      "happen.") %
                    (self.name, tank_type_label, product.display_name,
                     self.oil_product_id.display_name))
        return qty
Example #36
0
    def _get_orderpoint_action(self):
        """Create manual orderpoints for missing product in each warehouses. It also removes
        orderpoints that have been replenish. In order to do it:
        - It uses the report.stock.quantity to find missing quantity per product/warehouse
        - It checks if orderpoint already exist to refill this location.
        - It checks if it exists other sources (e.g RFQ) tha refill the warehouse.
        - It creates the orderpoints for missing quantity that were not refill by an upper option.

        return replenish report ir.actions.act_window
        """
        action = self.env["ir.actions.actions"]._for_xml_id(
            "stock.action_orderpoint_replenish")
        action['context'] = self.env.context
        orderpoints = self.env['stock.warehouse.orderpoint'].search([])
        # Remove previous automatically created orderpoint that has been refilled.
        to_remove = orderpoints.filtered(
            lambda o: o.create_uid.id == SUPERUSER_ID and o.qty_to_order <= 0.0
            and o.trigger == 'manual')
        to_remove.unlink()
        orderpoints = orderpoints - to_remove
        to_refill = defaultdict(float)
        qty_by_product_warehouse = self.env[
            'report.stock.quantity'].read_group(
                [('date', '=', fields.date.today()),
                 ('state', '=', 'forecast')],
                ['product_id', 'product_qty', 'warehouse_id'],
                ['product_id', 'warehouse_id'],
                lazy=False)
        for group in qty_by_product_warehouse:
            warehouse_id = group.get(
                'warehouse_id') and group['warehouse_id'][0]
            if group['product_qty'] >= 0.0 or not warehouse_id:
                continue
            to_refill[(group['product_id'][0],
                       warehouse_id)] = group['product_qty']
        if not to_refill:
            return action

        # Remove incoming quantity from other otigin than moves (e.g RFQ)
        product_ids, warehouse_ids = zip(*to_refill)
        # lot_stock_ids = [lot_stock_id_by_warehouse[w] for w in warehouse_ids]
        dummy, qty_by_product_wh = self.env['product.product'].browse(
            product_ids)._get_quantity_in_progress(warehouse_ids=warehouse_ids)
        rounding = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        for (product, warehouse), product_qty in to_refill.items():
            qty_in_progress = qty_by_product_wh.get(
                (product, warehouse)) or 0.0
            qty_in_progress += sum(
                orderpoints.filtered(lambda o: o.product_id.id == product and o
                                     .warehouse_id.id == warehouse).mapped(
                                         'qty_to_order'))
            # Add qty to order for other orderpoint under this warehouse.
            if not qty_in_progress:
                continue
            to_refill[(product, warehouse)] = product_qty + qty_in_progress
        to_refill = {
            k: v
            for k, v in to_refill.items()
            if float_compare(v, 0.0, precision_digits=rounding) < 0.0
        }

        lot_stock_id_by_warehouse = self.env['stock.warehouse'].search_read(
            [('id', 'in', [g[1] for g in to_refill.keys()])], ['lot_stock_id'])
        lot_stock_id_by_warehouse = {
            w['id']: w['lot_stock_id'][0]
            for w in lot_stock_id_by_warehouse
        }

        product_qty_available = {}
        for warehouse, group in groupby(sorted(to_refill,
                                               key=lambda p_w: p_w[1]),
                                        key=lambda p_w: p_w[1]):
            products = self.env['product.product'].browse(
                [p for p, w in group])
            products_qty_available_list = products.with_context(
                location=lot_stock_id_by_warehouse[warehouse]).mapped(
                    'qty_available')
            product_qty_available.update({
                (p.id, warehouse): q
                for p, q in zip(products, products_qty_available_list)
            })

        orderpoint_values_list = []
        for (product, warehouse), product_qty in to_refill.items():
            lot_stock_id = lot_stock_id_by_warehouse[warehouse]
            orderpoint = self.filtered(lambda o: o.product_id == product and o.
                                       location_id == lot_stock_id)
            if orderpoint:
                orderpoint[0].qty_forecast += product_qty
            else:
                orderpoint_values = self.env[
                    'stock.warehouse.orderpoint']._get_orderpoint_values(
                        product, lot_stock_id)
                orderpoint_values.update({
                    'name':
                    _('Replenishment Report'),
                    'warehouse_id':
                    warehouse,
                    'company_id':
                    self.env['stock.warehouse'].browse(
                        warehouse).company_id.id,
                })
            orderpoint_values_list.append(orderpoint_values)

        orderpoints = self.env['stock.warehouse.orderpoint'].with_user(
            SUPERUSER_ID).create(orderpoint_values_list)
        for orderpoint in orderpoints:
            orderpoint.route_id = orderpoint.product_id.route_ids[:1]
        orderpoints.filtered(lambda o: not o.route_id)._set_default_route_id()
        return action
Example #37
0
    def _procure_orderpoint_confirm(self,
                                    use_new_cursor=False,
                                    company_id=None,
                                    raise_user_error=True):
        """ 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.
        """
        self = self.with_company(company_id)
        orderpoints_noprefetch = self.read(['id'])
        orderpoints_noprefetch = [
            orderpoint['id'] for orderpoint in orderpoints_noprefetch
        ]

        for orderpoints_batch in split_every(1000, orderpoints_noprefetch):
            if use_new_cursor:
                cr = registry(self._cr.dbname).cursor()
                self = self.with_env(self.env(cr=cr))
            orderpoints_batch = self.env['stock.warehouse.orderpoint'].browse(
                orderpoints_batch)
            orderpoints_exceptions = []
            while orderpoints_batch:
                procurements = []
                for orderpoint in orderpoints_batch:
                    if float_compare(orderpoint.qty_to_order,
                                     0.0,
                                     precision_rounding=orderpoint.product_uom.
                                     rounding) == 1:
                        date = datetime.combine(orderpoint.lead_days_date,
                                                time.min)
                        values = orderpoint._prepare_procurement_values(
                            date=date)
                        procurements.append(
                            self.env['procurement.group'].Procurement(
                                orderpoint.product_id, orderpoint.qty_to_order,
                                orderpoint.product_uom, orderpoint.location_id,
                                orderpoint.name, orderpoint.name,
                                orderpoint.company_id, values))

                try:
                    with self.env.cr.savepoint():
                        self.env['procurement.group'].with_context(
                            from_orderpoint=True).run(
                                procurements,
                                raise_user_error=raise_user_error)
                except ProcurementException as errors:
                    for procurement, error_msg in errors.procurement_exceptions:
                        orderpoints_exceptions += [
                            (procurement.values.get('orderpoint_id'),
                             error_msg)
                        ]
                    failed_orderpoints = self.env[
                        'stock.warehouse.orderpoint'].concat(
                            *[o[0] for o in orderpoints_exceptions])
                    if not failed_orderpoints:
                        _logger.error('Unable to process orderpoints')
                        break
                    orderpoints_batch -= failed_orderpoints

                except OperationalError:
                    if use_new_cursor:
                        cr.rollback()
                        continue
                    else:
                        raise
                else:
                    orderpoints_batch._post_process_scheduler()
                    break

            # Log an activity on product template for failed orderpoints.
            for orderpoint, error_msg in orderpoints_exceptions:
                existing_activity = self.env['mail.activity'].search([
                    ('res_id', '=', orderpoint.product_id.product_tmpl_id.id),
                    ('res_model_id', '=',
                     self.env.ref('product.model_product_template').id),
                    ('note', '=', error_msg)
                ])
                if not existing_activity:
                    orderpoint.product_id.product_tmpl_id.activity_schedule(
                        'mail.mail_activity_data_warning',
                        note=error_msg,
                        user_id=orderpoint.product_id.responsible_id.id
                        or SUPERUSER_ID,
                    )

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

        return {}
Example #38
0
    def _create_or_update_finished_line(self):
        """
        1. Check that the final lot and the quantity producing is valid regarding
            other workorders of this production
        2. Save final lot and quantity producing to suggest on next workorder
        """
        self.ensure_one()
        final_lot_quantity = self.qty_production
        rounding = self.product_uom_id.rounding
        # Get the max quantity possible for current lot in other workorders
        for workorder in (self.production_id.workorder_ids - self):
            # We add the remaining quantity to the produced quantity for the
            # current lot. For 5 finished products: if in the first wo it
            # creates 4 lot A and 1 lot B and in the second it create 3 lot A
            # and it remains 2 units to product, it could produce 5 lot A.
            # In this case we select 4 since it would conflict with the first
            # workorder otherwise.
            line = workorder.finished_workorder_line_ids.filtered(
                lambda line: line.lot_id == self.finished_lot_id)
            line_without_lot = workorder.finished_workorder_line_ids.filtered(
                lambda line: line.product_id == workorder.product_id and
                not line.lot_id)
            quantity_remaining = workorder.qty_remaining + line_without_lot.qty_done
            quantity = line.qty_done + quantity_remaining
            if line and float_compare(quantity,
                                      final_lot_quantity,
                                      precision_rounding=rounding) <= 0:
                final_lot_quantity = quantity
            elif float_compare(quantity_remaining,
                               final_lot_quantity,
                               precision_rounding=rounding) < 0:
                final_lot_quantity = quantity_remaining

        # final lot line for this lot on this workorder.
        current_lot_lines = self.finished_workorder_line_ids.filtered(
            lambda line: line.lot_id == self.finished_lot_id)

        # this lot has already been produced
        if float_compare(final_lot_quantity,
                         current_lot_lines.qty_done + self.qty_producing,
                         precision_rounding=rounding) < 0:
            raise UserError(
                _('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one'
                  ) % (final_lot_quantity, self.product_id.uom_id.name,
                       self.finished_lot_id.name,
                       current_lot_lines.qty_done + self.qty_producing))

        # Update workorder line that regiter final lot created
        if not current_lot_lines:
            current_lot_lines = self.env['mrp.workorder.line'].create({
                'finished_workorder_id':
                self.id,
                'product_id':
                self.product_id.id,
                'lot_id':
                self.finished_lot_id.id,
                'qty_done':
                self.qty_producing,
            })
        else:
            current_lot_lines.qty_done += self.qty_producing
Example #39
0
 def _create_extra_move(self):
     ''' Creates an extra move if necessary depending on extra quantities than foreseen or extra moves'''
     self.ensure_one()
     quantity_to_split = 0
     uom_qty_to_split = 0
     extra_move = self.env['stock.move']
     rounding = self.product_uom.rounding
     link_procurement = False
     # If more produced than the procurement linked, you should create an extra move
     if self.procurement_id and self.production_id and float_compare(
             self.production_id.qty_produced,
             self.procurement_id.product_qty,
             precision_rounding=rounding) > 0:
         done_moves_total = sum(
             self.production_id.move_finished_ids.filtered(
                 lambda x: x.product_id == self.product_id and x.state ==
                 'done').mapped('product_uom_qty'))
         # If you depassed the quantity before, you don't need to split anymore, but adapt the quantities
         if float_compare(done_moves_total,
                          self.procurement_id.product_qty,
                          precision_rounding=rounding) >= 0:
             quantity_to_split = 0
             if float_compare(self.product_uom_qty,
                              self.quantity_done,
                              precision_rounding=rounding) < 0:
                 self.product_uom_qty = self.quantity_done  #TODO: could change qty on move_dest_id also (in case of 2-step in/out)
         else:
             quantity_to_split = done_moves_total + self.quantity_done - self.procurement_id.product_qty
             uom_qty_to_split = self.product_uom_qty - (
                 self.quantity_done - quantity_to_split
             )  #self.product_uom_qty - (self.procurement_id.product_qty + done_moves_total)
             if float_compare(uom_qty_to_split,
                              quantity_to_split,
                              precision_rounding=rounding) < 0:
                 uom_qty_to_split = quantity_to_split
             self.product_uom_qty = self.quantity_done - quantity_to_split
     # You split also simply  when the quantity done is bigger than foreseen
     elif float_compare(self.quantity_done,
                        self.product_uom_qty,
                        precision_rounding=rounding) > 0:
         quantity_to_split = self.quantity_done - self.product_uom_qty
         uom_qty_to_split = quantity_to_split  # + no need to change existing self.product_uom_qty
         link_procurement = True
     if quantity_to_split:
         extra_move = self.copy(
             default={
                 'quantity_done':
                 quantity_to_split,
                 'product_uom_qty':
                 uom_qty_to_split,
                 'production_id':
                 self.production_id.id,
                 'raw_material_production_id':
                 self.raw_material_production_id.id,
                 'procurement_id':
                 link_procurement and self.procurement_id.id or False
             })
         extra_move.action_confirm()
         if self.has_tracking != 'none':
             qty_todo = self.quantity_done - quantity_to_split
             for movelot in self.move_lot_ids.filtered(lambda x: x.done_wo):
                 if movelot.quantity_done and movelot.done_wo:
                     if float_compare(qty_todo,
                                      movelot.quantity_done,
                                      precision_rounding=rounding) >= 0:
                         qty_todo -= movelot.quantity_done
                     elif float_compare(
                             qty_todo, 0, precision_rounding=rounding) > 0:
                         #split
                         remaining = movelot.quantity_done - qty_todo
                         movelot.quantity_done = qty_todo
                         movelot.copy(
                             default={
                                 'move_id': extra_move.id,
                                 'quantity_done': remaining
                             })
                         qty_todo = 0
                     else:
                         movelot.move_id = extra_move.id
         else:
             self.quantity_done -= quantity_to_split
     return extra_move
Example #40
0
 def test_invoice_with_grouping(self):
     invoice = self.inv_model.create({
         'date_invoice':
         self._date('01-01'),
         'account_id':
         self.account_receivable.id,
         'partner_id':
         self.env.ref('base.res_partner_2').id,
         'journal_id':
         self.sale_journal.id,
         'type':
         'out_invoice',
         'invoice_line_ids': [
             (0, 0, {
                 'product_id': self.maint_product.id,
                 'name': 'Maintenance IPBX 12 mois',
                 'price_unit': 2400,
                 'quantity': 1,
                 'account_id': self.account_revenue.id,
                 'start_date': self._date('01-01'),
                 'end_date': self._date('12-31'),
             }),
             (0, 0, {
                 'product_id': self.maint_product.id,
                 'name': 'Maintenance téléphones 12 mois',
                 'price_unit': 12,
                 'quantity': 10,
                 'account_id': self.account_revenue.id,
                 'start_date': self._date('01-01'),
                 'end_date': self._date('12-31'),
             }),
             (0, 0, {
                 'product_id': self.maint_product.id,
                 'name': 'Maintenance Fax 6 mois',
                 'price_unit': 120.75,
                 'quantity': 1,
                 'account_id': self.account_revenue.id,
                 'start_date': self._date('01-01'),
                 'end_date': self._date('06-30'),
             }),
             (0, 0, {
                 'product_id': self.env.ref('product.product_product_5').id,
                 'name': 'HD IPBX',
                 'price_unit': 215.5,
                 'quantity': 1,
                 'account_id': self.account_revenue.id,
             }),
         ],
     })
     invoice.action_invoice_open()
     self.assertTrue(invoice.move_id)
     iline_res = {
         (self._date('01-01'), self._date('12-31')): 2520,
         (self._date('01-01'), self._date('06-30')): 120.75,
         (False, False): 215.5,
     }
     precision = self.env['decimal.precision'].precision_get('Account')
     for mline in invoice.move_id.line_ids:
         if mline.account_id == self.account_revenue:
             amount = iline_res.pop(
                 (fields.Date.to_string(mline.start_date),
                  fields.Date.to_string(mline.end_date)))
             self.assertEquals(
                 float_compare(amount,
                               mline.credit,
                               precision_digits=precision), 0)
Example #41
0
    def validate(self):
        self.ensure_one()
        assert self.state == 'produce'
        pr_oil = self.env['decimal.precision'].precision_get(
            'Olive Oil Volume')
        origin = _('Olive oil bottling wizard')
        mpo = self.env['mrp.production']
        splo = self.env['stock.production.lot']
        sqo = self.env['stock.quant']
        smo = self.env['stock.move']
        mblo = self.env['mrp.bom.line']
        oil_product = self.oil_product_id
        bottle_product = self.bottle_product_id
        bom = self.bom_id
        if self.quantity <= 0:
            raise UserError(
                _("The quantity of bottles to produce must be positive."))
        assert self.src_location_id
        assert self.other_src_location_id
        assert self.dest_location_id
        assert self.expiry_date
        assert self.lot_name
        assert oil_product.tracking == 'lot'
        assert bottle_product.tracking == 'lot'
        if self.expiry_date < fields.Date.context_today(self):
            raise UserError(_("The expiry date should not be in the past."))
        oil_start_qty_in_tank = self.src_location_id.olive_oil_tank_check(
            raise_if_empty=True,
            raise_if_reservation=True,
            raise_if_multi_lot=True)
        # Check we have enough oil
        oil_required_qty = self.quantity * self.bottle_volume
        if float_compare(oil_start_qty_in_tank,
                         oil_required_qty,
                         precision_digits=pr_oil) <= 0:
            raise UserError(
                _("The tank %s currently contains %s liters. This is not "
                  "enough for this bottling (%s liters required).") %
                (self.src_location_id.name, oil_start_qty_in_tank,
                 oil_required_qty))
        # Check we have enough empty bottles
        other_product_bom_lines = mblo.search([('bom_id', '=', bom.id),
                                               ('product_id', '!=',
                                                oil_product.id)])
        for bom_line in other_product_bom_lines:
            qty_required = self.quantity * bom_line.product_qty
            qrg = sqo.read_group(
                [('location_id', '=', self.other_src_location_id.id),
                 ('product_id', '=', bom_line.product_id.id),
                 ('reservation_id', '=', False)], ['qty'], [])

            free_start_qty = qrg and qrg[0]['qty'] or 0
            uom = bom_line.product_id.uom_id
            if float_compare(free_start_qty, qty_required,
                             precision_digits=0) <= 0:
                raise UserError(
                    _("The stock location '%s' contains %s %s '%s' without reservation. "
                      "This is not enough for this bottling (%s %s required).")
                    % (self.other_src_location_id.display_name, free_start_qty,
                       uom.name, bom_line.product_id.name, qty_required,
                       uom.name))
        mo = mpo.create({
            'product_id': bottle_product.id,
            'product_qty': self.quantity,
            'product_uom_id': bottle_product.uom_id.id,
            'location_src_id': self.src_location_id.id,
            'location_dest_id': self.dest_location_id.id,
            'origin': origin,
            'bom_id': bom.id,
        })
        assert mo.state == 'confirmed'
        assert len(mo.move_raw_ids) > 0, 'Missing raw moves'
        assert len(mo.move_finished_ids) == 1, 'Wrong finished moves'
        assert mo.move_finished_ids[
            0].product_id == bottle_product, 'Wrong product on finished move'
        oil_raw_move = smo.search([('product_id.olive_type', '=', 'oil'),
                                   ('raw_material_production_id', '=', mo.id)])
        other_raw_moves = smo.search([('product_id.olive_type', '!=', 'oil'),
                                      ('raw_material_production_id', '=',
                                       mo.id)])
        for rmove in other_raw_moves:
            if rmove.product_id.tracking in ('lot', 'serial'):
                raise UserError(
                    _("The bill of material has the component '%s' "
                      "which is tracked by lot or serial. For the moment, "
                      "the only supported scenario is where the only component "
                      "of the bill of material tracked by lot is the oil.") %
                    rmove.product_id.display_name)
        # BOM has already been checked, so this should really never happen
        assert len(oil_raw_move) == 1, 'Wrong number of oil raw moves'
        # HACK change source location for other raw moves
        other_raw_moves.write({'location_id': self.other_src_location_id.id})
        mo.action_assign()
        if mo.availability != 'assigned':
            raise UserError(
                _("Could not reserve the raw material for this bottling operation. "
                  "Check that you have enough oil and empty bottles."))
        for move_lot in oil_raw_move.move_lot_ids:
            assert move_lot.lot_id
            move_lot.quantity_done = move_lot.quantity
        for rmove in other_raw_moves:
            rmove.quantity_done = rmove.product_uom_qty
        # raw lines should be green at this step
        # Create finished lot
        new_lot = splo.create({
            'product_id': bottle_product.id,
            'name': self.lot_name,
            'expiry_date': self.expiry_date,
        })
        self.env['stock.move.lots'].create({
            'move_id': mo.move_finished_ids[0].id,
            'product_id': bottle_product.id,
            'production_id': mo.id,
            'quantity': self.quantity,
            'quantity_done': self.quantity,
            'lot_id': new_lot.id,
        })
        for raw_move_lot in oil_raw_move.move_lot_ids:
            assert not raw_move_lot.lot_produced_id
        oil_raw_move.move_lot_ids.write({'lot_produced_id': new_lot.id})
        mo.write({
            'state': 'progress',
            'date_start': datetime.now(),
        })
        assert mo.post_visible is True
        mo.post_inventory()
        assert mo.check_to_done is True
        mo.button_mark_done()

        # Check oil end qty
        oil_end_qty_in_tank = self.src_location_id.olive_oil_tank_check()
        if float_compare(oil_end_qty_in_tank,
                         oil_start_qty_in_tank - oil_required_qty,
                         precision_digits=pr_oil):
            raise UserError(
                _("The end quantity in tank (%s L) is wrong. This should never happen."
                  ) % oil_end_qty_in_tank)

        action = self.env['ir.actions.act_window'].for_xml_id(
            'mrp', 'mrp_production_action')
        action.update({
            'res_id': mo.id,
            'views': False,
            'view_mode': 'form,tree,kanban,calendar',
        })
        return action
Example #42
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(new_quantity_done,
                             ml.product_uom_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)
Example #43
0
    def test_order_to_invoice(self):

        # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl)
        self.pos_order_pos1 = self.PosOrder.create({
            'company_id':
            self.company_id,
            'partner_id':
            self.partner1.id,
            'pricelist_id':
            self.partner1.property_product_pricelist.id,
            'lines': [(0, 0, {
                'name': "OL/0001",
                'product_id': self.product3.id,
                'price_unit': 450,
                'discount': 5.0,
                'qty': 2.0,
                'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
            }),
                      (0, 0, {
                          'name': "OL/0002",
                          'product_id': self.product4.id,
                          'price_unit': 300,
                          'discount': 5.0,
                          'qty': 3.0,
                          'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
                      })]
        })

        # I click on the "Make Payment" wizard to pay the PoS order
        context_make_payment = {
            "active_ids": [self.pos_order_pos1.id],
            "active_id": self.pos_order_pos1.id
        }
        self.pos_make_payment = self.PosMakePayment.with_context(
            context_make_payment).create({
                'amount': (450 * 2 + 300 * 3 * 1.05) * 0.95,
            })
        # I click on the validate button to register the payment.
        context_payment = {'active_id': self.pos_order_pos1.id}
        self.pos_make_payment.with_context(context_payment).check()

        # I check that the order is marked as paid and there is no invoice
        # attached to it
        self.assertEqual(self.pos_order_pos1.state, 'paid',
                         "Order should be in paid state.")
        self.assertFalse(self.pos_order_pos1.invoice_id,
                         'Invoice should not be attached to order.')

        # I generate an invoice from the order
        self.invoice = self.pos_order_pos1.action_pos_order_invoice()

        # I test that the total of the attached invoice is correct
        self.amount_total = self.pos_order_pos1.amount_total
        self.assertEqual(
            float_compare(self.amount_total, 1752.75, precision_digits=2), 0,
            "Invoice not correct")
        """In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports"""

        # I select the period and journal for the bank statement

        context_journal = {'journal_type': 'bank'}
        self.assertTrue(
            self.AccountBankStatement.with_context(
                context_journal)._default_journal(),
            'Journal has not been selected')
        journal = self.env['account.journal'].create({
            'name':
            'Bank Test',
            'code':
            'BNKT',
            'type':
            'bank',
            'company_id':
            self.company_id,
        })
        # I create a bank statement with Opening and Closing balance 0.
        account_statement = self.AccountBankStatement.create({
            'balance_start':
            0.0,
            'balance_end_real':
            0.0,
            'date':
            time.strftime('%Y-%m-%d'),
            'journal_id':
            journal.id,
            'company_id':
            self.company_id,
            'name':
            'pos session test',
        })
        # I create bank statement line
        account_statement_line = self.AccountBankStatementLine.create({
            'amount':
            1000,
            'partner_id':
            self.partner4.id,
            'statement_id':
            account_statement.id,
            'name':
            'EXT001'
        })
        # I modify the bank statement and set the Closing Balance.
        account_statement.write({
            'balance_end_real': 1000.0,
        })

        # I reconcile the bank statement.
        new_aml_dicts = [{
            'account_id':
            self.partner4.property_account_receivable_id.id,
            'name':
            "EXT001",
            'credit':
            1000.0,
            'debit':
            0.0,
        }]

        account_statement_line.process_reconciliations([{
            'new_aml_dicts':
            new_aml_dicts
        }])

        # I confirm the bank statement using Confirm button

        self.AccountBankStatement.button_confirm_bank()
    def test_predictive_lead_scoring(self):
        """ We test here computation of lead probability based on PLS Bayes.
                We will use 3 different values for each possible variables:
                country_id : 1,2,3
                state_id: 1,2,3
                email_state: correct, incorrect, None
                phone_state: correct, incorrect, None
                source_id: 1,2,3
                stage_id: 1,2,3 + the won stage
                And we will compute all of this for 2 different team_id
            Note : We assume here that original bayes computation is correct
            as we don't compute manually the probabilities."""
        Lead = self.env['crm.lead']
        LeadScoringFrequency = self.env['crm.lead.scoring.frequency']
        state_values = ['correct', 'incorrect', None]
        source_ids = self.env['utm.source'].search([], limit=3).ids
        state_ids = self.env['res.country.state'].search([], limit=3).ids
        country_ids = self.env['res.country'].search([], limit=3).ids
        stage_ids = self.env['crm.stage'].search([], limit=3).ids
        won_stage_id = self.env['crm.stage'].search([('is_won', '=', True)],
                                                    limit=1).id
        team_ids = self.env['crm.team'].create([{
            'name': 'Team Test 1'
        }, {
            'name': 'Team Test 2'
        }]).ids
        # create bunch of lost and won crm_lead
        leads_to_create = []
        #   for team 1
        for i in range(3):
            leads_to_create.append(
                self._get_lead_values(team_ids[0], 'team_1_%s' % str(i),
                                      country_ids[i], state_ids[i],
                                      state_values[i], state_values[i],
                                      source_ids[i], stage_ids[i]))
        leads_to_create.append(
            self._get_lead_values(team_ids[0], 'team_1_%s' % str(3),
                                  country_ids[0], state_ids[1],
                                  state_values[2], state_values[0],
                                  source_ids[2], stage_ids[1]))
        leads_to_create.append(
            self._get_lead_values(team_ids[0], 'team_1_%s' % str(4),
                                  country_ids[1], state_ids[1],
                                  state_values[1], state_values[0],
                                  source_ids[1], stage_ids[0]))
        #   for team 2
        leads_to_create.append(
            self._get_lead_values(team_ids[1], 'team_2_%s' % str(5),
                                  country_ids[0], state_ids[1],
                                  state_values[2], state_values[0],
                                  source_ids[1], stage_ids[2]))
        leads_to_create.append(
            self._get_lead_values(team_ids[1], 'team_2_%s' % str(6),
                                  country_ids[0], state_ids[1],
                                  state_values[0], state_values[1],
                                  source_ids[2], stage_ids[1]))
        leads_to_create.append(
            self._get_lead_values(team_ids[1], 'team_2_%s' % str(7),
                                  country_ids[0], state_ids[2],
                                  state_values[0], state_values[1],
                                  source_ids[2], stage_ids[0]))
        leads_to_create.append(
            self._get_lead_values(team_ids[1], 'team_2_%s' % str(8),
                                  country_ids[0], state_ids[1],
                                  state_values[2], state_values[0],
                                  source_ids[2], stage_ids[1]))
        leads_to_create.append(
            self._get_lead_values(team_ids[1], 'team_2_%s' % str(9),
                                  country_ids[1], state_ids[0],
                                  state_values[1], state_values[0],
                                  source_ids[1], stage_ids[1]))

        leads = Lead.create(leads_to_create)

        # Set the PLS config
        self.env['ir.config_parameter'].sudo().set_param(
            "crm.pls_start_date", "2000-01-01")
        self.env['ir.config_parameter'].sudo().set_param(
            "crm.pls_fields",
            "country_id,state_id,email_state,phone_state,source_id,tag_ids")

        # set leads as won and lost
        # for Team 1
        leads[0].action_set_lost()
        leads[1].action_set_lost()
        leads[2].action_set_won()
        # for Team 2
        leads[5].action_set_lost()
        leads[6].action_set_lost()
        leads[7].action_set_won()

        # A. Test Full Rebuild
        # rebuild frequencies table and recompute automated_probability for all leads.
        Lead._cron_update_automated_probabilities()

        # As the cron is computing and writing in SQL queries, we need to invalidate the cache
        leads.invalidate_cache()

        self.assertEqual(
            tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)

        # Test frequencies
        lead_4_stage_0_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[4].team_id.id),
            ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])
        ])
        lead_4_stage_won_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[4].team_id.id),
            ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)
        ])
        lead_4_country_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[4].team_id.id),
            ('variable', '=', 'country_id'),
            ('value', '=', leads[4].country_id.id)
        ])
        lead_4_email_state_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[4].team_id.id),
            ('variable', '=', 'email_state'),
            ('value', '=', str(leads[4].email_state))
        ])

        lead_9_stage_0_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[9].team_id.id),
            ('variable', '=', 'stage_id'), ('value', '=', stage_ids[0])
        ])
        lead_9_stage_won_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[9].team_id.id),
            ('variable', '=', 'stage_id'), ('value', '=', won_stage_id)
        ])
        lead_9_country_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[9].team_id.id),
            ('variable', '=', 'country_id'),
            ('value', '=', leads[9].country_id.id)
        ])
        lead_9_email_state_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[9].team_id.id),
            ('variable', '=', 'email_state'),
            ('value', '=', str(leads[9].email_state))
        ])

        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)
        self.assertEqual(lead_4_country_freq.won_count, 0.1)
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)
        self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)
        self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1)
        self.assertEqual(lead_4_country_freq.lost_count, 1.1)
        self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)

        self.assertEqual(lead_9_stage_0_freq.won_count, 1.1)
        self.assertEqual(lead_9_stage_won_freq.won_count, 1.1)
        self.assertEqual(lead_9_country_freq.won_count,
                         0.0)  # frequency does not exist
        self.assertEqual(lead_9_email_state_freq.won_count, 1.1)
        self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)
        self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)
        self.assertEqual(lead_9_country_freq.lost_count,
                         0.0)  # frequency does not exist
        self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)

        # B. Test Live Increment
        leads[4].action_set_lost()
        leads[9].action_set_won()

        # re-get frequencies that did not exists before
        lead_9_country_freq = LeadScoringFrequency.search([
            ('team_id', '=', leads[9].team_id.id),
            ('variable', '=', 'country_id'),
            ('value', '=', leads[9].country_id.id)
        ])

        # B.1. Test frequencies - team 1 should not impact team 2
        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.won_count, 0.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1)  # + 1
        self.assertEqual(
            lead_4_stage_won_freq.lost_count,
            0.1)  # unchanged - consider stages with <= sequence when lost
        self.assertEqual(lead_4_country_freq.lost_count, 2.1)  # + 1
        self.assertEqual(lead_4_email_state_freq.lost_count, 3.1)  # + 1

        self.assertEqual(lead_9_stage_0_freq.won_count, 2.1)  # + 1
        self.assertEqual(lead_9_stage_won_freq.won_count,
                         2.1)  # + 1 - consider every stages when won
        self.assertEqual(lead_9_country_freq.won_count, 1.1)  # + 1
        self.assertEqual(lead_9_email_state_freq.won_count, 2.1)  # + 1
        self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)  # unchanged
        self.assertEqual(lead_9_country_freq.lost_count,
                         0.1)  # unchanged (did not exists before)
        self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)  # unchanged

        # Propabilities of other leads should not be impacted as only modified lead are recomputed.
        self.assertEqual(
            tools.float_compare(leads[3].automated_probability, 33.49, 2), 0)
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 7.74, 2), 0)

        self.assertEqual(leads[3].is_automated_probability, True)
        self.assertEqual(leads[8].is_automated_probability, True)

        # Restore -> Should decrease lost
        leads[4].toggle_active()
        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.won_count, 0.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)  # - 1
        self.assertEqual(
            lead_4_stage_won_freq.lost_count,
            0.1)  # unchanged - consider stages with <= sequence when lost
        self.assertEqual(lead_4_country_freq.lost_count, 1.1)  # - 1
        self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)  # - 1

        self.assertEqual(lead_9_stage_0_freq.won_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_won_freq.won_count, 2.1)  # unchanged
        self.assertEqual(lead_9_country_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_9_email_state_freq.won_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)  # unchanged
        self.assertEqual(lead_9_country_freq.lost_count, 0.1)  # unchanged
        self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)  # unchanged

        # set to won stage -> Should increase won
        leads[4].stage_id = won_stage_id
        self.assertEqual(lead_4_stage_0_freq.won_count, 2.1)  # + 1
        self.assertEqual(lead_4_stage_won_freq.won_count, 2.1)  # + 1
        self.assertEqual(lead_4_country_freq.won_count, 1.1)  # + 1
        self.assertEqual(lead_4_email_state_freq.won_count, 2.1)  # + 1
        self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.lost_count, 0.1)  # unchanged
        self.assertEqual(lead_4_country_freq.lost_count, 1.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)  # unchanged

        # Archive (was won, now lost) -> Should decrease won and increase lost
        leads[4].toggle_active()
        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)  # - 1
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)  # - 1
        self.assertEqual(lead_4_country_freq.won_count, 0.1)  # - 1
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)  # - 1
        self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1)  # + 1
        self.assertEqual(
            lead_4_stage_won_freq.lost_count, 1.1
        )  # consider stages with <= sequence when lostand as stage is won.. even won_stage lost_count is increased by 1
        self.assertEqual(lead_4_country_freq.lost_count, 2.1)  # + 1
        self.assertEqual(lead_4_email_state_freq.lost_count, 3.1)  # + 1

        # Move to original stage -> Should do nothing (as lead is still lost)
        leads[4].stage_id = stage_ids[0]
        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.won_count, 0.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_0_freq.lost_count, 3.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.lost_count, 2.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.lost_count, 3.1)  # unchanged

        # Restore -> Should decrease lost - at the end, frequencies should be like first frequencyes tests (except for 0.0 -> 0.1)
        leads[4].toggle_active()
        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.won_count, 0.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)  # - 1
        self.assertEqual(
            lead_4_stage_won_freq.lost_count,
            1.1)  # unchanged - consider stages with <= sequence when lost
        self.assertEqual(lead_4_country_freq.lost_count, 1.1)  # - 1
        self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)  # - 1

        # Probabilities should only be recomputed after modifying the lead itself.
        leads[3].stage_id = stage_ids[
            0]  # probability should only change a bit as frequencies are almost the same (except 0.0 -> 0.1)
        leads[8].stage_id = stage_ids[
            0]  # probability should change quite a lot

        # Test frequencies (should not have changed)
        self.assertEqual(lead_4_stage_0_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.won_count, 0.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_4_stage_0_freq.lost_count, 2.1)  # unchanged
        self.assertEqual(lead_4_stage_won_freq.lost_count, 1.1)  # unchanged
        self.assertEqual(lead_4_country_freq.lost_count, 1.1)  # unchanged
        self.assertEqual(lead_4_email_state_freq.lost_count, 2.1)  # unchanged

        self.assertEqual(lead_9_stage_0_freq.won_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_won_freq.won_count, 2.1)  # unchanged
        self.assertEqual(lead_9_country_freq.won_count, 1.1)  # unchanged
        self.assertEqual(lead_9_email_state_freq.won_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_0_freq.lost_count, 2.1)  # unchanged
        self.assertEqual(lead_9_stage_won_freq.lost_count, 0.1)  # unchanged
        self.assertEqual(lead_9_country_freq.lost_count, 0.1)  # unchanged
        self.assertEqual(lead_9_email_state_freq.lost_count, 2.1)  # unchanged

        # Continue to test probability computation
        leads[3].probability = 40

        self.assertEqual(leads[3].is_automated_probability, False)
        self.assertEqual(leads[8].is_automated_probability, True)

        self.assertEqual(
            tools.float_compare(leads[3].automated_probability, 20.87, 2), 0)
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
        self.assertEqual(tools.float_compare(leads[3].probability, 40, 2), 0)
        self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)

        # Test modify country_id
        leads[8].country_id = country_ids[1]
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 34.38, 2), 0)
        self.assertEqual(tools.float_compare(leads[8].probability, 34.38, 2),
                         0)

        leads[8].country_id = country_ids[0]
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 2.43, 2), 0)
        self.assertEqual(tools.float_compare(leads[8].probability, 2.43, 2), 0)

        # ----------------------------------------------
        # Test tag_id frequencies and probability impact
        # ----------------------------------------------

        tag_ids = self.env['crm.tag'].create([
            {
                'name': "Tag_test_1"
            },
            {
                'name': "Tag_test_2"
            },
        ]).ids
        # tag_ids = self.env['crm.tag'].search([], limit=2).ids
        leads_with_tags = self.generate_leads_with_tags(tag_ids)

        leads_with_tags[:30].action_set_lost()  # 60% lost on tag 1
        leads_with_tags[31:50].action_set_won()  # 40% won on tag 1
        leads_with_tags[50:90].action_set_lost()  # 80% lost on tag 2
        leads_with_tags[91:100].action_set_won()  # 20% won on tag 2
        leads_with_tags[100:135].action_set_lost()  # 70% lost on tag 1 and 2
        leads_with_tags[136:150].action_set_won()  # 30% won on tag 1 and 2
        # tag 1 : won = 19+14  /  lost = 30+35
        # tag 2 : won = 9+14  /  lost = 40+35

        tag_1_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'),
                                                  ('value', '=', tag_ids[0])])
        tag_2_freq = LeadScoringFrequency.search([('variable', '=', 'tag_id'),
                                                  ('value', '=', tag_ids[1])])
        self.assertEqual(tools.float_compare(tag_1_freq.won_count, 33.1, 1), 0)
        self.assertEqual(tools.float_compare(tag_1_freq.lost_count, 65.1, 1),
                         0)
        self.assertEqual(tools.float_compare(tag_2_freq.won_count, 23.1, 1), 0)
        self.assertEqual(tools.float_compare(tag_2_freq.lost_count, 75.1, 1),
                         0)

        # Force recompute - A priori, no need to do this as, for each won / lost, we increment tag frequency.
        Lead._cron_update_automated_probabilities()
        leads_with_tags.invalidate_cache()

        lead_tag_1 = leads_with_tags[30]
        lead_tag_2 = leads_with_tags[90]
        lead_tag_1_2 = leads_with_tags[135]

        self.assertEqual(
            tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2),
            0)

        lead_tag_1.tag_ids = [(5, 0, 0)]  # remove all tags
        lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)]  # remove tag 2

        self.assertEqual(
            tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_2.automated_probability, 23.51, 2),
            0)  # no impact
        self.assertEqual(
            tools.float_compare(lead_tag_1_2.automated_probability, 33.69, 2),
            0)

        lead_tag_1.tag_ids = [(4, tag_ids[1])]  # add tag 2
        lead_tag_2.tag_ids = [(4, tag_ids[0])]  # add tag 1
        lead_tag_1_2.tag_ids = [(3, tag_ids[0]),
                                (4, tag_ids[1])]  # remove tag 1 / add tag 2

        self.assertEqual(
            tools.float_compare(lead_tag_1.automated_probability, 23.51, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_2.automated_probability, 28.05, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_1_2.automated_probability, 23.51, 2),
            0)

        # go back to initial situation
        lead_tag_1.tag_ids = [(3, tag_ids[1]),
                              (4, tag_ids[0])]  # remove tag 2 / add tag 1
        lead_tag_2.tag_ids = [(3, tag_ids[0])]  # remove tag 1
        lead_tag_1_2.tag_ids = [(4, tag_ids[0])]  # add tag 1

        self.assertEqual(
            tools.float_compare(lead_tag_1.automated_probability, 33.69, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_2.automated_probability, 23.51, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_1_2.automated_probability, 28.05, 2),
            0)

        # set email_state for each lead and update probabilities
        leads.filtered(lambda lead: lead.id % 2 == 0).email_state = 'correct'
        leads.filtered(lambda lead: lead.id % 2 == 1).email_state = 'incorrect'
        Lead._cron_update_automated_probabilities()
        leads_with_tags.invalidate_cache()

        self.assertEqual(
            tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)

        # remove all pls fields
        self.env['ir.config_parameter'].sudo().set_param(
            "crm.pls_fields", False)
        Lead._cron_update_automated_probabilities()
        leads_with_tags.invalidate_cache()

        self.assertEqual(
            tools.float_compare(leads[3].automated_probability, 34.38, 2), 0)
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 50.0, 2), 0)

        # check if the probabilities are the same with the old param
        self.env['ir.config_parameter'].sudo().set_param(
            "crm.pls_fields",
            "country_id,state_id,email_state,phone_state,source_id")
        Lead._cron_update_automated_probabilities()
        leads_with_tags.invalidate_cache()

        self.assertEqual(
            tools.float_compare(leads[3].automated_probability, 4.21, 2), 0)
        self.assertEqual(
            tools.float_compare(leads[8].automated_probability, 0.23, 2), 0)

        # remove tag_ids from the calculation
        self.assertEqual(
            tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2),
            0)

        lead_tag_1.tag_ids = [(5, 0, 0)]  # remove all tags
        lead_tag_2.tag_ids = [(4, tag_ids[0])]  # add tag 1
        lead_tag_1_2.tag_ids = [(3, tag_ids[1], 0)]  # remove tag 2

        self.assertEqual(
            tools.float_compare(lead_tag_1.automated_probability, 28.6, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_2.automated_probability, 28.6, 2), 0)
        self.assertEqual(
            tools.float_compare(lead_tag_1_2.automated_probability, 28.6, 2),
            0)
    def _prepare_transaction(self, line, speeddict, action='create'):
        bdio = self.env['business.document.import']
        account_analytic_id = account_id = False
        # convert to float
        float_fields = [
            'vat_eur', 'amount_eur', 'amount_currency', 'vat_20_id',
            'vat_10_id', 'vat_55_id', 'vat_21_id'
        ]
        for float_field in float_fields:
            if line.get(float_field):
                try:
                    line[float_field] = float(line[float_field])
                except Exception:
                    raise UserError(
                        _("Cannot convert float field '%s' with value '%s'.") %
                        (float_field, line.get(float_field)))
            else:
                line[float_field] = 0.0
        total_vat_rates = line['vat_20_id'] + line['vat_10_id'] +\
            line['vat_55_id'] + line['vat_21_id']
        if float_compare(line['vat_eur'], total_vat_rates, precision_digits=2):
            raise UserError(
                _("Error in the Mooncard CSV file: for transaction ID '%s' "
                  "the column 'vat_eur' (%.2f) doesn't have the same value "
                  "as the sum of the 4 columns per VAT rate (%.2f)") %
                (line['id'], line['vat_eur'], total_vat_rates))
        if line.get('charge_account'):
            account = bdio._match_account({'code': line['charge_account']}, [],
                                          speed_dict=speeddict['accounts'])
            account_id = account.id
        if line.get('analytic_code_1'):
            account_analytic_id = speeddict['analytic'].get(
                line['analytic_code_1'].lower())
        ttype2odoo = {
            'P': 'presentment',
            'L': 'load',
        }
        if line.get('transaction_type') not in ttype2odoo:
            raise UserError(
                _("Wrong transaction type '%s'. The only possible values are "
                  "'P' (presentment) or 'L' (load).") %
                line.get('transaction_type'))
        transaction_type = ttype2odoo[line['transaction_type']]
        vals = {
            'transaction_type': transaction_type,
            'description': line.get('title'),
            'expense_categ_name': line.get('expense_category_name'),
            'expense_account_id': account_id,
            'account_analytic_id': account_analytic_id,
            'vat_company_currency': line['vat_eur'],
            'fr_vat_20_amount': line['vat_20_id'],
            'fr_vat_10_amount': line['vat_10_id'],
            'fr_vat_5_5_amount': line['vat_55_id'],
            'fr_vat_2_1_amount': line['vat_21_id'],
            'image_url': line.get('attachment'),
            'receipt_number': line.get('receipt_code'),
        }

        if action == 'update':
            return vals

        # Continue with fields required for create
        country_id = False
        if line.get('country_code') and len(line['country_code']) == 3:
            logger.debug('search country with code %s with pycountry',
                         line['country_code'])
            pcountry = pycountry.countries.get(alpha_3=line['country_code'])
            if pcountry and pcountry.alpha_2:
                country_id = speeddict['countries'].get(pcountry.alpha_2)
        currency_id = speeddict['currencies'].get(
            line.get('original_currency'))
        card_id = False
        if line.get('card_token'):
            card_id = speeddict['tokens'].get(line['card_token'])
            if not card_id:
                raise UserError(
                    _("The CSV file contains the Moon Card '%s'. This "
                      "card is not registered in Odoo, cf menu "
                      "Accounting > Configuration > Miscellaneous > "
                      "Moon Cards)") % line.get('card_token'))
        payment_date = False
        if (transaction_type == 'presentment'
                and line.get('date_authorization')):
            payment_date = self.convert_datetime_to_utc(
                line['date_authorization'])

        vals.update({
            'unique_import_id':
            line.get('id'),
            'date':
            self.convert_datetime_to_utc(line['date_transaction']),
            'payment_date':
            payment_date,
            'card_id':
            card_id,
            'country_id':
            country_id,
            'merchant':
            line.get('supplier'),
            'total_company_currency':
            line['amount_eur'],
            'total_currency':
            line['amount_currency'],
            'currency_id':
            currency_id,
        })
        return vals
Example #46
0
    def _create_extra_move_lines(self):
        """Create new sml if quantity produced is bigger than the reserved one"""
        vals_list = []
        # apply putaway
        location_dest_id = self.move_id.location_dest_id._get_putaway_strategy(
            self.product_id) or self.move_id.location_dest_id
        quants = self.env['stock.quant']._gather(self.product_id,
                                                 self.move_id.location_id,
                                                 lot_id=self.lot_id,
                                                 strict=False)
        # Search for a sub-locations where the product is available.
        # Loop on the quants to get the locations. If there is not enough
        # quantity into stock, we take the move location. Anyway, no
        # reservation is made, so it is still possible to change it afterwards.
        for quant in quants:
            quantity = quant.quantity - quant.reserved_quantity
            quantity = self.product_id.uom_id._compute_quantity(
                quantity, self.product_uom_id, rounding_method='HALF-UP')
            rounding = quant.product_uom_id.rounding
            if (float_compare(quant.quantity, 0, precision_rounding=rounding)
                    <= 0 or float_compare(
                        quantity,
                        0,
                        precision_rounding=self.product_uom_id.rounding) <= 0):
                continue
            vals = {
                'move_id': self.move_id.id,
                'product_id': self.product_id.id,
                'location_id': quant.location_id.id,
                'location_dest_id': location_dest_id.id,
                'product_uom_qty': 0,
                'product_uom_id': self.product_uom_id.id,
                'qty_done': min(quantity, self.qty_done),
                'lot_produced_ids': self._get_produced_lots(),
            }
            if self.lot_id:
                vals.update({'lot_id': self.lot_id.id})

            vals_list.append(vals)
            self.qty_done -= vals['qty_done']
            # If all the qty_done is distributed, we can close the loop
            if float_compare(
                    self.qty_done,
                    0,
                    precision_rounding=self.product_id.uom_id.rounding) <= 0:
                break

        if float_compare(
                self.qty_done,
                0,
                precision_rounding=self.product_id.uom_id.rounding) > 0:
            vals = {
                'move_id': self.move_id.id,
                'product_id': self.product_id.id,
                'location_id': self.move_id.location_id.id,
                'location_dest_id': location_dest_id.id,
                'product_uom_qty': 0,
                'product_uom_id': self.product_uom_id.id,
                'qty_done': self.qty_done,
                'lot_produced_ids': self._get_produced_lots(),
            }
            if self.lot_id:
                vals.update({'lot_id': self.lot_id.id})

            vals_list.append(vals)

        return vals_list
    def olive_oil_transfer(self,
                           dest_loc,
                           transfer_type,
                           warehouse,
                           dest_partner=False,
                           partial_transfer_qty=False,
                           origin=False,
                           auto_validate=False):
        self.ensure_one()
        assert transfer_type in ('partial', 'full'), 'wrong transfer_type arg'
        if dest_loc == self:
            raise UserError(
                _("You are trying to transfer oil from '%s' to the same location!"
                  ) % self.display_name)
        sqo = self.env['stock.quant']
        smo = self.env['stock.move']
        pr_oil = self.env['decimal.precision'].precision_get(
            'Olive Oil Volume')
        src_loc = self
        raise_if_not_merged = False
        if transfer_type == 'partial':
            raise_if_not_merged = True
        src_qty = src_loc.olive_oil_tank_check(
            raise_if_not_merged=raise_if_not_merged)
        # compat src/dest
        if dest_loc.olive_tank_type:
            dest_loc.olive_oil_tank_check(raise_if_not_merged=False,
                                          raise_if_empty=False)
            dest_loc.olive_oil_tank_compatibility_check(
                src_loc.oil_product_id, src_loc.olive_season_id)

        if not warehouse.int_type_id:
            raise UserError(
                _("Internal picking type not configured on warehouse %s.") %
                warehouse.display_name)
        vals = {
            'picking_type_id': warehouse.int_type_id.id,
            'origin': origin,
            'location_id': src_loc.id,
            'location_dest_id': dest_loc.id,
        }
        pick = self.env['stock.picking'].create(vals)

        if transfer_type == 'full':
            quants = sqo.search([('location_id', '=', src_loc.id)])
            for quant in quants:
                if float_compare(quant.qty, 0, precision_digits=2) < 0:
                    raise UserError(
                        _("There is a negative quant ID %d on olive tank %s. "
                          "This should never happen.") %
                        (quant.id, src_loc.display_name))
                if quant.reservation_id:
                    raise UserError(
                        _("There is a reserved quant ID %d on olive tank %s. "
                          "This must be investigated before trying a tank "
                          "transfer again.") %
                        (quant.id, src_loc.display_name))
                mvals = {
                    'name': _('Full oil tank transfer'),
                    'origin': origin,
                    'product_id': quant.product_id.id,
                    'location_id': src_loc.id,
                    'location_dest_id': dest_loc.id,
                    'product_uom': quant.product_id.uom_id.id,
                    'product_uom_qty': quant.qty,
                    'restrict_lot_id': quant.lot_id.id or False,
                    'restrict_partner_id': quant.owner_id.id or False,
                    'picking_id': pick.id,
                }
                move = smo.create(mvals)
                qvals = {'reservation_id': move.id}
                if dest_partner and quant.owner_id != dest_partner:
                    qvals['owner_id'] = dest_partner.id
                quant.sudo().write(qvals)
        elif transfer_type == 'partial':
            # we already checked above that the src loc has 1 lot
            if float_compare(partial_transfer_qty, 0,
                             precision_digits=pr_oil) <= 0:
                raise UserError(
                    _("The quantity to transfer (%s L) must be strictly positive."
                      ) % partial_transfer_qty)
            if float_compare(partial_transfer_qty,
                             src_qty,
                             precision_digits=pr_oil) >= 0:
                raise UserError(
                    _("The quantity to transfer (%s L) from tank '%s' is superior "
                      "to its current oil quantity (%s L).") %
                    (partial_transfer_qty, src_loc.name, src_qty))
            product = src_loc.oil_product_id
            mvals = {
                'name': _('Partial oil tank transfer'),
                'origin': origin,
                'product_id': product.id,
                'location_id': src_loc.id,
                'location_dest_id': dest_loc.id,
                'product_uom': product.uom_id.id,
                'product_uom_qty': partial_transfer_qty,
                'picking_id': pick.id,
            }
            move = smo.create(mvals)
            # No need to reserve a particular quant, because we only have 1 lot
            # Hack for dest_partner is at the end of the method
        pick.action_confirm()
        pick.action_assign()
        pick.action_pack_operation_auto_fill()
        if auto_validate:
            pick.do_transfer()
            if transfer_type == 'partial' and dest_partner:
                move.quant_ids.sudo().write({'owner_id': dest_partner.id})
        elif transfer_type == 'partial' and dest_partner:
            raise UserError(
                "We don't support partial transferts without auto_validate and "
                "with dest_partner")
        return pick
Example #48
0
    def _update_workorder_lines(self):
        """ Update workorder lines, according to the new qty currently
        produced. It returns a dict with line to create, update or delete.
        It do not directly write or unlink the line because this function is
        used in onchange and request that write on db (e.g. workorder creation).
        """
        line_values = {'to_create': [], 'to_delete': [], 'to_update': {}}
        # moves are actual records
        move_finished_ids = self.move_finished_ids._origin.filtered(
            lambda move: move.product_id != self.product_id and move.state
            not in ('done', 'cancel'))
        move_raw_ids = self.move_raw_ids._origin.filtered(
            lambda move: move.state not in ('done', 'cancel'))
        for move in move_raw_ids | move_finished_ids:
            move_workorder_lines = self._workorder_line_ids().filtered(
                lambda w: w.move_id == move)

            # Compute the new quantity for the current component
            rounding = move.product_uom.rounding
            new_qty = self._prepare_component_quantity(move,
                                                       self.qty_producing)

            # In case the production uom is different than the workorder uom
            # it means the product is serial and production uom is not the reference
            new_qty = self.product_uom_id._compute_quantity(
                new_qty, self.production_id.product_uom_id, round=False)
            qty_todo = float_round(
                new_qty - sum(move_workorder_lines.mapped('qty_to_consume')),
                precision_rounding=rounding)

            # Remove or lower quantity on exisiting workorder lines
            if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
                qty_todo = abs(qty_todo)
                # Try to decrease or remove lines that are not reserved and
                # partialy reserved first. A different decrease strategy could
                # be define in _unreserve_order method.
                for workorder_line in move_workorder_lines.sorted(
                        key=lambda wl: wl._unreserve_order()):
                    if float_compare(qty_todo, 0,
                                     precision_rounding=rounding) <= 0:
                        break
                    # If the quantity to consume on the line is lower than the
                    # quantity to remove, the line could be remove.
                    if float_compare(workorder_line.qty_to_consume,
                                     qty_todo,
                                     precision_rounding=rounding) <= 0:
                        qty_todo = float_round(qty_todo -
                                               workorder_line.qty_to_consume,
                                               precision_rounding=rounding)
                        if line_values['to_delete']:
                            line_values['to_delete'] |= workorder_line
                        else:
                            line_values['to_delete'] = workorder_line
                    # decrease the quantity on the line
                    else:
                        new_val = workorder_line.qty_to_consume - qty_todo
                        # avoid to write a negative reserved quantity
                        new_reserved = max(
                            0, workorder_line.qty_reserved - qty_todo)
                        line_values['to_update'][workorder_line] = {
                            'qty_to_consume': new_val,
                            'qty_done': new_val,
                            'qty_reserved': new_reserved,
                        }
                        qty_todo = 0
            else:
                # Search among wo lines which one could be updated
                qty_reserved_wl = defaultdict(float)
                # Try to update the line with the greater reservation first in
                # order to promote bigger batch.
                for workorder_line in move_workorder_lines.sorted(
                        key=lambda wl: wl.qty_reserved, reverse=True):
                    rounding = workorder_line.product_uom_id.rounding
                    if float_compare(qty_todo, 0,
                                     precision_rounding=rounding) <= 0:
                        break
                    move_lines = workorder_line._get_move_lines()
                    qty_reserved_wl[
                        workorder_line.lot_id] += workorder_line.qty_reserved
                    # The reserved quantity according to exisiting move line
                    # already produced (with qty_done set) and other production
                    # lines with the same lot that are currently on production.
                    qty_reserved_remaining = sum(
                        move_lines.mapped('product_uom_qty')) - sum(
                            move_lines.mapped('qty_done')) - qty_reserved_wl[
                                workorder_line.lot_id]
                    if float_compare(qty_reserved_remaining,
                                     0,
                                     precision_rounding=rounding) > 0:
                        qty_to_add = min(qty_reserved_remaining, qty_todo)
                        line_values['to_update'][workorder_line] = {
                            'qty_done':
                            workorder_line.qty_to_consume + qty_to_add,
                            'qty_to_consume':
                            workorder_line.qty_to_consume + qty_to_add,
                            'qty_reserved':
                            workorder_line.qty_reserved + qty_to_add,
                        }
                        qty_todo -= qty_to_add
                        qty_reserved_wl[workorder_line.lot_id] += qty_to_add

                    # If a line exists without reservation and without lot. It
                    # means that previous operations could not find any reserved
                    # quantity and created a line without lot prefilled. In this
                    # case, the system will not find an existing move line with
                    # available reservation anymore and will increase this line
                    # instead of creating a new line without lot and reserved
                    # quantities.
                    if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial':
                        line_values['to_update'][workorder_line] = {
                            'qty_done':
                            workorder_line.qty_to_consume + qty_todo,
                            'qty_to_consume':
                            workorder_line.qty_to_consume + qty_todo,
                        }
                        qty_todo = 0

                # if there are still qty_todo, create new wo lines
                if float_compare(qty_todo, 0.0,
                                 precision_rounding=rounding) > 0:
                    for values in self._generate_lines_values(move, qty_todo):
                        line_values['to_create'].append(values)
        # wo lines without move_id should also be deleted
        for wo_line in self._workorder_line_ids().filtered(
                lambda w: not w.move_id and
            (not w.finished_workorder_id or w.product_id != w.
             finished_workorder_id.product_id)):
            if not line_values['to_delete']:
                line_values['to_delete'] = wo_line
            elif wo_line not in line_values['to_delete']:
                line_values['to_delete'] |= wo_line
        return line_values
Example #49
0
    def record_production(self):
        if not self:
            return True

        self.ensure_one()
        self._check_sn_uniqueness()
        self._check_company()
        if float_compare(self.qty_producing,
                         0,
                         precision_rounding=self.product_uom_id.rounding) <= 0:
            raise UserError(
                _('Please set the quantity you are currently producing. It should be different from zero.'
                  ))
        if 'check_ids' not in self:
            for line in self.raw_workorder_line_ids | self.finished_workorder_line_ids:
                line._check_line_sn_uniqueness()
        # If last work order, then post lots used
        if not self.next_work_order_id:
            self._update_finished_move()

        # Transfer quantities from temporary to final move line or make them final
        self._update_moves()

        # Transfer lot (if present) and quantity produced to a finished workorder line
        if self.product_tracking != 'none':
            self._create_or_update_finished_line()

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

        # Suggest a finished lot on the next workorder
        if self.next_work_order_id and self.production_id.product_id.tracking != 'none' and not self.next_work_order_id.finished_lot_id:
            self.next_work_order_id._defaults_from_finished_workorder_line(
                self.finished_workorder_line_ids)
            # As we may have changed the quantity to produce on the next workorder,
            # make sure to update its wokorder lines
            self.next_work_order_id._apply_update_workorder_lines()

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

        # Test if the production is done
        rounding = self.production_id.product_uom_id.rounding
        if float_compare(self.qty_produced,
                         self.production_id.product_qty,
                         precision_rounding=rounding) < 0:
            previous_wo = self.env['mrp.workorder']
            if self.product_tracking != 'none':
                previous_wo = self.env['mrp.workorder'].search([
                    ('next_work_order_id', '=', self.id)
                ])
            candidate_found_in_previous_wo = False
            if previous_wo:
                candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(
                    previous_wo.finished_workorder_line_ids)
            if not candidate_found_in_previous_wo:
                # self is the first workorder
                self.qty_producing = self.qty_remaining
                self.finished_lot_id = False
                if self.product_tracking == 'serial':
                    self.qty_producing = 1

            self._apply_update_workorder_lines()
        else:
            self.qty_producing = 0
            self.button_finish()
        return True
Example #50
0
    def _update_move_lines(self):
        """ update a move line to save the workorder line data"""
        self.ensure_one()
        if self.lot_id:
            move_lines = self.move_id.move_line_ids.filtered(
                lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_ids
            )
        else:
            move_lines = self.move_id.move_line_ids.filtered(
                lambda ml: not ml.lot_id and not ml.lot_produced_ids)

        # Sanity check: if the product is a serial number and `lot` is already present in the other
        # consumed move lines, raise.
        if self.product_id.tracking != 'none' and not self.lot_id:
            raise UserError(
                _('Please enter a lot or serial number for %s !' %
                  self.product_id.display_name))

        if self.lot_id and self.product_id.tracking == 'serial' and self.lot_id in self.move_id.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.'
                  ))

        # Update reservation and quantity done
        for ml in move_lines:
            rounding = ml.product_uom_id.rounding
            if float_compare(self.qty_done, 0,
                             precision_rounding=rounding) <= 0:
                break
            quantity_to_process = min(self.qty_done,
                                      ml.product_uom_qty - ml.qty_done)
            self.qty_done -= quantity_to_process

            new_quantity_done = (ml.qty_done + quantity_to_process)
            # if we produce less than the reserved quantity to produce the finished products
            # in different lots,
            # we create different component_move_lines to record which one was used
            # on which lot of finished product
            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_ids': self._get_produced_lots(),
                })
            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_ids': self._get_produced_lots(),
                }
                ml.copy(default=default)
                ml.with_context(bypass_reservation_update=True).write({
                    'product_uom_qty':
                    new_qty_reserved,
                    'qty_done':
                    0
                })
    def test_action_confirm_finish_phonecall(self):
        tag = 'helpdesk_phonecall_support.helpdesk_phonecall_service_tag_01'

        start_date_hour = '2017-09-04 14:49:20'
        finish_date_hour = '2017-09-04 15:20:40'

        values = {
            'partner_id': self.partner.id,
            'contact_partner_id': self.partner_contact.id,
            'project_id': self.project.id,
            'description': 'Teste',
            'phonecall_tag_id': self.env.ref(tag).id,
            'start_date_hour': start_date_hour,
        }

        # Finalmente criamos um atendimento
        phonecall = self.env['helpdesk.phonecall.service'].create(values)

        # Referencia para o wizard
        wizard = self.env['helpdesk.phonecall.confirm'].with_context(
            {'active_ids': [phonecall.id]})

        # Executamos o metodo de finalizar o atendimento. Utilizamos o mock
        # para retornamos a data que queremos para que assim seja mais facil
        # verificar se o calculo de data esta correto
        with mock.patch('odoo.fields.Datetime.now') as dt:
            dt.return_value = finish_date_hour
            wizard.action_confirm_finish_phonecall()

        self.assertTrue(phonecall.finish_date_hour)
        self.assertEqual(phonecall.state, 'done')
        self.assertEqual(phonecall.finish_date_hour, finish_date_hour)

        # Executamos o metodo de finalizar o atendimento, verificamos
        # se ele obedece o conceito que apenas atendimentos em aberto podem
        # ser finalizados
        with self.assertRaises(UserError):
            wizard.action_confirm_finish_phonecall()

        timesheet = self.env['account.analytic.line'].search([
            ('helpdesk_id', '=', phonecall.id)
        ])

        self.assertEqual(phonecall.description, timesheet.name)
        self.assertEqual(phonecall.user_id.id, timesheet.user_id.id)
        self.assertEqual(phonecall.partner_id.id, timesheet.partner_id.id)
        self.assertEqual(phonecall.project_id.id, timesheet.project_id.id)
        self.assertEqual(phonecall.company_id.id, timesheet.company_id.id)
        self.assertEqual(phonecall.start_date_hour[:10], timesheet.date)

        # Verificamos a diferença de datas em segundos e a convertemos para
        # horas, já que o widget 'float_time' do Odoo considera o valor de
        # horas em segundos
        hours_diff_decimal = 1880 / 3600.0

        result = float_compare(timesheet.unit_amount,
                               hours_diff_decimal,
                               precision_digits=12)

        # Verificamos se o resultado da comparacao e zero. No Python o valor
        # e considerado False
        self.assertFalse(result)
Example #52
0
 def onchange_price(self):
     '''当订单行的不含税单价改变时,改变含税单价'''
     price = self.price_taxed / (1 + self.tax_rate * 0.01)  # 不含税单价
     decimal = self.env.ref('core.decimal_price')
     if float_compare(price, self.price, precision_digits=decimal.digits) != 0:
         self.price_taxed = self.price * (1 + self.tax_rate * 0.01)
 def _prepare_move(self, line):
     category_id = line.asset_id.category_id
     account_analytic_id = line.asset_id.account_analytic_id
     analytic_tag_ids = line.asset_id.analytic_tag_ids
     depreciation_date = self.env.context.get(
         'depreciation_date'
     ) or line.depreciation_date or fields.Date.context_today(self)
     company_currency = line.asset_id.company_id.currency_id
     current_currency = line.asset_id.currency_id
     prec = company_currency.decimal_places
     amount = current_currency._convert(line.amount, company_currency,
                                        line.asset_id.company_id,
                                        depreciation_date)
     asset_name = line.asset_id.name + ' (%s/%s)' % (
         line.sequence, len(line.asset_id.depreciation_line_ids))
     move_line_1 = {
         'name':
         asset_name,
         'account_id':
         category_id.account_depreciation_id.id,
         'debit':
         0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else
         -amount,
         'credit':
         amount
         if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
         'partner_id':
         line.asset_id.partner_id.id,
         'analytic_account_id':
         account_analytic_id.id if category_id.type == 'sale' else False,
         'analytic_tag_ids':
         [(6, 0,
           analytic_tag_ids.ids)] if category_id.type == 'sale' else False,
         'currency_id':
         company_currency != current_currency and current_currency.id
         or False,
         'amount_currency':
         company_currency != current_currency and -1.0 * line.amount or 0.0,
     }
     move_line_2 = {
         'name':
         asset_name,
         'account_id':
         category_id.account_depreciation_expense_id.id,
         'credit':
         0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else
         -amount,
         'debit':
         amount
         if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0,
         'partner_id':
         line.asset_id.partner_id.id,
         'analytic_account_id':
         account_analytic_id.id
         if category_id.type == 'purchase' else False,
         'analytic_tag_ids': [(6, 0, analytic_tag_ids.ids)]
         if category_id.type == 'purchase' else False,
         'currency_id':
         company_currency != current_currency and current_currency.id
         or False,
         'amount_currency':
         company_currency != current_currency and line.amount or 0.0,
     }
     move_vals = {
         'ref': line.asset_id.code,
         'date': depreciation_date or False,
         'journal_id': category_id.journal_id.id,
         'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)],
     }
     return move_vals
Example #54
0
    def test_bom_report(self):
        """ Simulate a crumble receipt with mrp and open the bom structure
        report and check that data insde are correct.
        """
        uom_kg = self.env.ref('uom.product_uom_kgm')
        uom_litre = self.env.ref('uom.product_uom_litre')
        crumble = self.env['product.product'].create({
            'name': 'Crumble',
            'type': 'product',
            'uom_id': uom_kg.id,
            'uom_po_id': uom_kg.id,
        })
        butter = self.env['product.product'].create({
            'name': 'Butter',
            'type': 'product',
            'uom_id': uom_kg.id,
            'uom_po_id': uom_kg.id,
            'standard_price': 7.01
        })
        biscuit = self.env['product.product'].create({
            'name': 'Biscuit',
            'type': 'product',
            'uom_id': uom_kg.id,
            'uom_po_id': uom_kg.id,
            'standard_price': 1.5
        })
        bom_form_crumble = Form(self.env['mrp.bom'])
        bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id
        bom_form_crumble.product_qty = 11
        bom_form_crumble.product_uom_id = uom_kg
        bom_crumble = bom_form_crumble.save()

        with Form(bom_crumble) as bom:
            with bom.bom_line_ids.new() as line:
                line.product_id = butter
                line.product_uom_id = uom_kg
                line.product_qty = 5
            with bom.bom_line_ids.new() as line:
                line.product_id = biscuit
                line.product_uom_id = uom_kg
                line.product_qty = 6

        workcenter = self.env['mrp.workcenter'].create({
            'costs_hour': 10,
            'name': 'Deserts Table'
        })

        routing_form = Form(self.env['mrp.routing'])
        routing_form.name = "Crumble process"
        routing_crumble = routing_form.save()

        with Form(routing_crumble) as routing:
            with routing.operation_ids.new() as operation:
                operation.workcenter_id = workcenter
                operation.name = 'Prepare biscuits'
                operation.time_cycle_manual = 5
            with routing.operation_ids.new() as operation:
                operation.workcenter_id = workcenter
                operation.name = 'Prepare butter'
                operation.time_cycle_manual = 3
            with routing.operation_ids.new() as operation:
                operation.workcenter_id = workcenter
                operation.name = 'Mix manually'
                operation.time_cycle_manual = 5

        bom_crumble.routing_id = routing_crumble.id

        # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
        report_values = self.env[
            'report.mrp.report_bom_structure']._get_report_data(
                bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
        # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
        self.assertEqual(
            report_values['lines']['operations_time'], 13.0,
            'Operation time should be the same for 1 unit or for the batch')
        # Operation cost is the sum of operation line.
        operation_cost = float_round(5 / 60 * 10,
                                     precision_digits=2) * 2 + float_round(
                                         3 / 60 * 10, precision_digits=2)
        self.assertEqual(
            float_compare(report_values['lines']['operations_cost'],
                          operation_cost,
                          precision_digits=2), 0,
            '13 minute for 10$/hours -> 2.16')

        for component_line in report_values['lines']['components']:
            # standard price * bom line quantity * current quantity / bom finished product quantity
            if component_line['prod_id'] == butter.id:
                # 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
                self.assertEqual(
                    float_compare(component_line['total'], (7.01 * 5),
                                  precision_digits=2), 0)
            if component_line['prod_id'] == biscuit.id:
                # 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
                self.assertEqual(
                    float_compare(component_line['total'], (1.5 * 6),
                                  precision_digits=2), 0)
        # total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21
        self.assertEqual(
            float_compare(report_values['lines']['total'],
                          46.21,
                          precision_digits=2), 0,
            'Product Bom Price is not correct')
        self.assertEqual(
            float_compare(report_values['lines']['total'] / 11.0,
                          4.20,
                          precision_digits=2), 0,
            'Product Unit Bom Price is not correct')

        # TEST BOM STRUCTURE VALUE BY UNIT
        report_values = self.env[
            'report.mrp.report_bom_structure']._get_report_data(
                bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
        # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
        self.assertEqual(
            report_values['lines']['operations_time'], 13.0,
            'Operation time should be the same for 1 unit or for the batch')
        # Operation cost is the sum of operation line.
        operation_cost = float_round(5 / 60 * 10,
                                     precision_digits=2) * 2 + float_round(
                                         3 / 60 * 10, precision_digits=2)
        self.assertEqual(
            float_compare(report_values['lines']['operations_cost'],
                          operation_cost,
                          precision_digits=2), 0,
            '13 minute for 10$/hours -> 2.16')

        for component_line in report_values['lines']['components']:
            # standard price * bom line quantity * current quantity / bom finished product quantity
            if component_line['prod_id'] == butter.id:
                # 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
                self.assertEqual(
                    float_compare(component_line['total'],
                                  (7.01 * 5) * (1 / 11),
                                  precision_digits=2), 0)
            if component_line['prod_id'] == biscuit.id:
                # 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
                self.assertEqual(
                    float_compare(component_line['total'],
                                  (1.5 * 6) * (1 / 11),
                                  precision_digits=2), 0)
        # total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
        self.assertEqual(
            float_compare(report_values['lines']['total'],
                          6.17,
                          precision_digits=2), 0,
            'Product Unit Bom Price is not correct')

        # TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
        report_values_12 = self.env[
            'report.mrp.report_bom_structure']._get_report_data(
                bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
        report_values_22 = self.env[
            'report.mrp.report_bom_structure']._get_report_data(
                bom_id=bom_crumble.id, searchQty=22, searchVariant=False)
        operation_cost = float_round(10 / 60 * 10,
                                     precision_digits=2) * 2 + float_round(
                                         6 / 60 * 10, precision_digits=2)
        # Both needs 2 operation cycle
        self.assertEqual(report_values_12['lines']['operations_cost'],
                         report_values_22['lines']['operations_cost'])
        self.assertEqual(report_values_22['lines']['operations_cost'],
                         operation_cost)
        report_values_23 = self.env[
            'report.mrp.report_bom_structure']._get_report_data(
                bom_id=bom_crumble.id, searchQty=23, searchVariant=False)
        operation_cost = float_round(15 / 60 * 10,
                                     precision_digits=2) * 2 + float_round(
                                         9 / 60 * 10, precision_digits=2)
        self.assertEqual(report_values_23['lines']['operations_cost'],
                         operation_cost)

        # Create a more complex BoM with a sub product
        cheese_cake = self.env['product.product'].create({
            'name': 'Cheese Cake 300g',
            'type': 'product',
        })
        cream = self.env['product.product'].create({
            'name': 'cream',
            'type': 'product',
            'uom_id': uom_litre.id,
            'uom_po_id': uom_litre.id,
            'standard_price': 5.17,
        })
        bom_form_cheese_cake = Form(self.env['mrp.bom'])
        bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id
        bom_form_cheese_cake.product_qty = 60
        bom_form_cheese_cake.product_uom_id = self.uom_unit
        bom_cheese_cake = bom_form_cheese_cake.save()

        with Form(bom_cheese_cake) as bom:
            with bom.bom_line_ids.new() as line:
                line.product_id = cream
                line.product_uom_id = uom_litre
                line.product_qty = 3
            with bom.bom_line_ids.new() as line:
                line.product_id = crumble
                line.product_uom_id = uom_kg
                line.product_qty = 5.4

        workcenter_2 = self.env['mrp.workcenter'].create({
            'name': 'cake mounting',
            'costs_hour': 20,
            'time_start': 10,
            'time_stop': 15
        })

        routing_form = Form(self.env['mrp.routing'])
        routing_form.name = "Cheese cake process"
        routing_cheese = routing_form.save()

        with Form(routing_cheese) as routing:
            with routing.operation_ids.new() as operation:
                operation.workcenter_id = workcenter
                operation.name = 'Mix cheese and crumble'
                operation.time_cycle_manual = 10
            with routing.operation_ids.new() as operation:
                operation.workcenter_id = workcenter_2
                operation.name = 'Cake mounting'
                operation.time_cycle_manual = 5

        bom_cheese_cake.routing_id = routing_cheese.id

        # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
        report_values = self.env[
            'report.mrp.report_bom_structure']._get_report_data(
                bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
        self.assertEqual(
            report_values['lines']['operations_time'], 40.0,
            'Operation time should be the same for 1 unit or for the batch')
        # Operation cost is the sum of operation line.
        operation_cost = float_round(
            10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20,
                                                            precision_digits=2)
        self.assertEqual(
            float_compare(report_values['lines']['operations_cost'],
                          operation_cost,
                          precision_digits=2), 0)

        for component_line in report_values['lines']['components']:
            # standard price * bom line quantity * current quantity / bom finished product quantity
            if component_line['prod_id'] == cream.id:
                # 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
                self.assertEqual(
                    float_compare(component_line['total'], (3 * 5.17),
                                  precision_digits=2), 0)
            if component_line['prod_id'] == crumble.id:
                # 5.4 kg of crumble at the cost of a batch.
                crumble_cost = self.env[
                    'report.mrp.report_bom_structure']._get_report_data(
                        bom_id=bom_crumble.id,
                        searchQty=5.4,
                        searchVariant=False)['lines']['total']
                self.assertEqual(
                    float_compare(component_line['total'],
                                  crumble_cost,
                                  precision_digits=2), 0)
        # total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost
        self.assertEqual(
            float_compare(report_values['lines']['total'],
                          27.18 + crumble_cost,
                          precision_digits=2), 0,
            'Product Bom Price is not correct')
    def action_payslip_done(self):
        res = super(HrPayslip, self).action_payslip_done()
        precision = self.env['decimal.precision'].precision_get('Payroll')

        for slip in self:
            line_ids = []
            debit_sum = 0.0
            credit_sum = 0.0
            date = slip.date or slip.date_to

            name = _('Payslip of %s') % (slip.employee_id.name)
            move_dict = {
                'narration': name,
                'ref': slip.number,
                'z_analytic_account_id':
                slip.employee_id.z_analytic_account_id.id,
                'journal_id': slip.journal_id.id,
                'date': date,
            }
            for line in slip.details_by_salary_rule_category:
                amount = slip.credit_note and -line.total or line.total
                if float_is_zero(amount, precision_digits=precision):
                    continue
                debit_account_id = line.salary_rule_id.account_debit.id
                credit_account_id = line.salary_rule_id.account_credit.id
                analytic_tag_ids = [
                    (4, analytic_tag.id, None)
                    for analytic_tag in slip.employee_id.z_analytic_tag_ids
                ]

                if debit_account_id:
                    debit_line = (0, 0, {
                        'name':
                        line.name,
                        'partner_id':
                        line._get_partner_id(credit_account=False),
                        'account_id':
                        debit_account_id,
                        'journal_id':
                        slip.journal_id.id,
                        'date':
                        date,
                        'debit':
                        amount > 0.0 and amount or 0.0,
                        'credit':
                        amount < 0.0 and -amount or 0.0,
                        'analytic_account_id':
                        slip.employee_id.z_analytic_account_id.id,
                        'analytic_tag_ids':
                        analytic_tag_ids,
                        'tax_line_id':
                        line.salary_rule_id.account_tax_id.id,
                    })
                    line_ids.append(debit_line)
                    debit_sum += debit_line[2]['debit'] - debit_line[2][
                        'credit']

                if credit_account_id:
                    credit_line = (0, 0, {
                        'name':
                        line.name,
                        'partner_id':
                        line._get_partner_id(credit_account=True),
                        'account_id':
                        credit_account_id,
                        'journal_id':
                        slip.journal_id.id,
                        'date':
                        date,
                        'debit':
                        amount < 0.0 and -amount or 0.0,
                        'credit':
                        amount > 0.0 and amount or 0.0,
                        'analytic_account_id':
                        slip.employee_id.z_analytic_account_id.id,
                        'analytic_tag_ids':
                        analytic_tag_ids,
                        'tax_line_id':
                        line.salary_rule_id.account_tax_id.id,
                    })
                    line_ids.append(credit_line)
                    credit_sum += credit_line[2]['credit'] - credit_line[2][
                        'debit']

            if float_compare(credit_sum, debit_sum,
                             precision_digits=precision) == -1:
                acc_id = slip.journal_id.default_credit_account_id.id
                if not acc_id:
                    raise UserError(
                        _('The Expense Journal "%s" has not properly configured the Credit Account!'
                          ) % (slip.journal_id.name))
                adjust_credit = (0, 0, {
                    'name': _('Adjustment Entry'),
                    'partner_id': False,
                    'account_id': acc_id,
                    'journal_id': slip.journal_id.id,
                    'date': date,
                    'debit': 0.0,
                    'credit': debit_sum - credit_sum,
                })
                line_ids.append(adjust_credit)

            elif float_compare(debit_sum,
                               credit_sum,
                               precision_digits=precision) == -1:
                acc_id = slip.journal_id.default_debit_account_id.id
                if not acc_id:
                    raise UserError(
                        _('The Expense Journal "%s" has not properly configured the Debit Account!'
                          ) % (slip.journal_id.name))
                adjust_debit = (0, 0, {
                    'name': _('Adjustment Entry'),
                    'partner_id': False,
                    'account_id': acc_id,
                    'journal_id': slip.journal_id.id,
                    'date': date,
                    'debit': credit_sum - debit_sum,
                    'credit': 0.0,
                })
                line_ids.append(adjust_debit)
            move_dict['line_ids'] = line_ids
            move = self.env['account.move'].create(move_dict)
            slip.write({'move_id': move.id, 'date': date})
            move.post()
        return res
Example #56
0
 def _check_done_allocation(self):
     precision = self.env['decimal.precision'].precision_get(
         'Product Unit of Measure')
     self.ensure_one()
     return self.allocation_ids and float_compare(
         self.qty_cancelled, 0, precision_digits=precision) > 0
Example #57
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'])
                                    values['responsible_moves'] = product_quantity[orderpoint.product_id.id]['responsible_moves']
                                    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 {}
    def default_get(self, fields):
        res = super(MrpProductProduce, self).default_get(fields)
        if self._context and self._context.get('active_id'):
            production = self.env['mrp.production'].browse(
                self._context['active_id'])
            serial_finished = (production.product_id.tracking == 'serial')
            if serial_finished:
                todo_quantity = 1.0
            else:
                main_product_moves = production.move_finished_ids.filtered(
                    lambda x: x.product_id.id == production.product_id.id)
                todo_quantity = production.product_qty - sum(
                    main_product_moves.mapped('quantity_done'))
                todo_quantity = todo_quantity if (todo_quantity > 0) else 0
            if 'production_id' in fields:
                res['production_id'] = production.id
            if 'product_id' in fields:
                res['product_id'] = production.product_id.id
            if 'product_uom_id' in fields:
                res['product_uom_id'] = production.product_uom_id.id
            if 'serial' in fields:
                res['serial'] = bool(serial_finished)
            if 'product_qty' in fields:
                res['product_qty'] = todo_quantity
            if 'produce_line_ids' in fields:
                lines = []
                for move in production.move_raw_ids.filtered(
                        lambda x: (x.product_id.tracking != 'none') and x.state
                        not in ('done', 'cancel') and x.bom_line_id):
                    qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty
                    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': 0.0,
                            'lot_id': move_line.lot_id.id,
                            'product_uom_id': move.product_uom.id,
                            'product_id': move.product_id.id,
                        })
                        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':
                                    0.0,
                                    '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': 0.0,
                                'product_uom_id': move.product_uom.id,
                                'product_id': move.product_id.id,
                            })

                res['produce_line_ids'] = [(0, 0, x) for x in lines]
        return res
Example #59
0
    def test_check(self):
        invoice1 = self.create_invoice(
            self.partner.id,
            self.bank,
            self.eur_currency.id,
            2042.0,
            "Inv9032",
        )
        invoice2 = self.create_invoice(
            self.partner.id,
            self.bank,
            self.eur_currency.id,
            1012.0,
            "Inv9033",
        )
        for inv in [invoice1, invoice2]:
            action = inv.create_account_payment_line()
        self.assertEqual(action["res_model"], "account.payment.order")
        self.payment_order = self.payment_order_model.browse(action["res_id"])
        self.assertEqual(self.payment_order.payment_type, "outbound")
        self.assertEqual(self.payment_order.payment_mode_id, self.payment_mode)
        self.assertEqual(self.payment_order.journal_id, self.bank_journal)
        pay_lines = self.payment_line_model.search(
            [
                ("partner_id", "=", self.partner.id),
                ("order_id", "=", self.payment_order.id),
            ]
        )
        self.assertEqual(len(pay_lines), 2)
        asus_pay_line1 = pay_lines[0]
        accpre = self.env["decimal.precision"].precision_get("Account")
        self.assertEqual(asus_pay_line1.currency_id, self.eur_currency)
        self.assertEqual(
            asus_pay_line1.partner_bank_id, invoice1.partner_bank_id
        )
        self.assertEqual(
            float_compare(
                asus_pay_line1.amount_currency, 2042, precision_digits=accpre
            ),
            0,
        )
        self.assertEqual(asus_pay_line1.communication_type, "normal")
        self.assertEqual(asus_pay_line1.communication, "Inv9032")
        self.payment_order.draft2open()
        self.assertEqual(self.payment_order.state, "open")
        bank_lines = self.bank_line_model.search(
            [("partner_id", "=", self.partner.id)]
        )
        self.assertEqual(len(bank_lines), 1)
        asus_bank_line = bank_lines[0]
        self.assertEqual(asus_bank_line.currency_id, self.eur_currency)
        self.assertEqual(
            float_compare(
                asus_bank_line.amount_currency, 3054.0, precision_digits=accpre
            ),
            0,
        )
        self.assertEqual(asus_bank_line.communication_type, "normal")
        self.assertEqual(asus_bank_line.communication, "Inv9032-Inv9033")
        self.assertEqual(
            asus_bank_line.partner_bank_id, invoice1.partner_bank_id
        )

        action = self.payment_order.open2generated()
        self.assertEqual(self.payment_order.state, "generated")
        self.assertEqual(action["res_model"], "ir.attachment")
        attachment = self.attachment_model.browse(action["res_id"])
        self.assertEqual(attachment.datas_fname[-4:], ".pdf")
Example #60
0
 def move_validate(self):
     ''' Validate moves based on a production order. '''
     moves = self._filter_closed_moves()
     quant_obj = self.env['stock.quant']
     moves_todo = self.env['stock.move']
     moves_to_unreserve = self.env['stock.move']
     # Create extra moves where necessary
     for move in moves:
         # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
         # it is possible that the user changed the value before posting the inventory by a value that should be
         # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
         # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be
         # consumed/produced and the moves are split correctly).
         rounding = move.product_uom.rounding
         move.quantity_done = float_round(move.quantity_done,
                                          precision_rounding=rounding,
                                          rounding_method='UP')
         if move.quantity_done <= 0:
             continue
         moves_todo |= move
         moves_todo |= move._create_extra_move()
     # Split moves where necessary and move quants
     for move in moves_todo:
         rounding = move.product_uom.rounding
         if float_compare(move.quantity_done,
                          move.product_uom_qty,
                          precision_rounding=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)
             new_move = move.split(qty_split)
             # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
             move.move_lot_ids.filtered(
                 lambda x: not x.done_wo or x.quantity_done == 0.0).write(
                     {'move_id': new_move})
             self.browse(new_move).quantity_done = 0.0
         main_domain = [('qty', '>', 0)]
         preferred_domain = [('reservation_id', '=', move.id)]
         fallback_domain = [('reservation_id', '=', False)]
         fallback_domain2 = [
             '&', ('reservation_id', '!=', move.id),
             ('reservation_id', '!=', False)
         ]
         preferred_domain_list = [preferred_domain] + [fallback_domain] + [
             fallback_domain2
         ]
         if move.has_tracking == 'none':
             quants = quant_obj.quants_get_preferred_domain(
                 move.product_qty,
                 move,
                 domain=main_domain,
                 preferred_domain_list=preferred_domain_list)
             self.env['stock.quant'].quants_move(
                 quants,
                 move,
                 move.location_dest_id,
                 owner_id=move.restrict_partner_id.id)
         else:
             for movelot in move.active_move_lot_ids:
                 if float_compare(movelot.quantity_done,
                                  0,
                                  precision_rounding=rounding) > 0:
                     if not movelot.lot_id:
                         raise UserError(
                             _('You need to supply a lot/serial number.'))
                     qty = move.product_uom._compute_quantity(
                         movelot.quantity_done, move.product_id.uom_id)
                     quants = quant_obj.quants_get_preferred_domain(
                         qty,
                         move,
                         lot_id=movelot.lot_id.id,
                         domain=main_domain,
                         preferred_domain_list=preferred_domain_list)
                     self.env['stock.quant'].quants_move(
                         quants,
                         move,
                         move.location_dest_id,
                         lot_id=movelot.lot_id.id,
                         owner_id=move.restrict_partner_id.id)
         moves_to_unreserve |= move
         # Next move in production order
         if move.move_dest_id and move.move_dest_id.state not in ('done',
                                                                  'cancel'):
             move.move_dest_id.action_assign()
     moves_to_unreserve.quants_unreserve()
     moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
     return moves_todo