Esempio n. 1
0
 def _check_package(self):
     default_uom = self.product_id.uom_id
     pack = self.product_packaging
     qty = self.product_uom_qty
     q = default_uom._compute_quantity(pack.qty, self.product_uom)
     # We do not use the modulo operator to check if qty is a mltiple of q. Indeed the quantity
     # per package might be a float, leading to incorrect results. For example:
     # 8 % 1.6 = 1.5999999999999996
     # 5.4 % 1.8 = 2.220446049250313e-16
     if (qty and q
             and float_compare(qty / q,
                               float_round(qty / q, precision_rounding=1.0),
                               precision_rounding=0.001) != 0):
         newqty = qty - (qty % q) + q
         return {
             'warning': {
                 'title':
                 _('Warning'),
                 'message':
                 _("This product is packaged by %.2f %s. You should sell %.2f %s."
                   ) % (pack.qty, default_uom.name, newqty,
                        self.product_uom.name),
             },
         }
     return {}
Esempio n. 2
0
 def _get_price(self, pricelist, product, qty):
     sale_price_digits = self.env['decimal.precision'].precision_get(
         'Product Price')
     price = pricelist.get_product_price(product, qty, False)
     if not price:
         price = product.list_price
     return float_round(price, precision_digits=sale_price_digits)
Esempio n. 3
0
 def _get_rounded_amount(self, amount):
     if self.config_id.cash_rounding:
         amount = float_round(
             amount,
             precision_rounding=self.config_id.rounding_method.rounding,
             rounding_method=self.config_id.rounding_method.rounding_method)
     return super(PosOrder, self)._get_rounded_amount(amount)
Esempio n. 4
0
    def _get_price(self, bom, factor, product):
        price = 0
        if bom.routing_id:
            # routing are defined on a BoM and don't have a concept of quantity.
            # It means that the operation time are defined for the quantity on
            # the BoM (the user produces a batch of products). E.g the user
            # product a batch of 10 units with a 5 minutes operation, the time
            # will be the 5 for a quantity between 1-10, then doubled for
            # 11-20,...
            operation_cycle = float_round(factor,
                                          precision_rounding=1,
                                          rounding_method='UP')
            operations = self._get_operation_line(bom.routing_id,
                                                  operation_cycle, 0)
            price += sum([op['total'] for op in operations])

        for line in bom.bom_line_ids:
            if line._skip_bom_line(product):
                continue
            if line.child_bom_id:
                qty = line.product_uom_id._compute_quantity(
                    line.product_qty * factor, line.child_bom_id.product_uom_id
                ) / line.child_bom_id.product_qty
                sub_price = self._get_price(line.child_bom_id, qty,
                                            line.product_id)
                price += sub_price
            else:
                prod_qty = line.product_qty * factor
                company = bom.company_id or self.env.company
                not_rounded_price = line.product_id.uom_id._compute_price(
                    line.product_id.with_context(
                        force_comany=company.id).standard_price,
                    line.product_uom_id) * prod_qty
                price += company.currency_id.round(not_rounded_price)
        return price
Esempio n. 5
0
    def round(self, amount):
        """Compute the rounding on the amount passed as parameter.

        :param amount: the amount to round
        :return: the rounded amount depending the rounding value and the rounding method
        """
        return float_round(amount, precision_rounding=self.rounding, rounding_method=self.rounding_method)
Esempio n. 6
0
 def _adyen_convert_amount(self, amount, currency):
     """
     Adyen requires the amount to be multiplied by 10^k,
     where k depends on the currency code.
     """
     k = CURRENCY_CODE_MAPS.get(currency.name, 2)
     paymentAmount = int(tools.float_round(amount, k) * (10**k))
     return paymentAmount
Esempio n. 7
0
 def try_round(amount, expected, digits=3, method='HALF-UP'):
     value = float_round(amount,
                         precision_digits=digits,
                         rounding_method=method)
     result = float_repr(value, precision_digits=digits)
     self.assertEqual(
         result, expected,
         'Rounding error: got %s, expected %s' % (result, expected))
Esempio n. 8
0
 def get_wallet_balance(self, user, include_config=True):
     result = float_round(sum(
         move['amount']
         for move in self.env['lunch.cashmove.report'].search_read([(
             'user_id', '=', user.id)], ['amount'])),
                          precision_digits=2)
     if include_config:
         result += user.company_id.lunch_minimum_threshold
     return result
Esempio n. 9
0
    def infos(self, user_id=None):
        self._check_user_impersonification(user_id)
        user = request.env['res.users'].browse(
            user_id) if user_id else request.env.user

        infos = self._make_infos(user, order=False)

        lines = self._get_current_lines(user.id)
        if lines:
            lines = [
                {
                    'id':
                    line.id,
                    'product':
                    (line.product_id.id, line.product_id.name,
                     float_repr(float_round(line.product_id.price, 2), 2)),
                    'toppings': [(topping.name,
                                  float_repr(float_round(topping.price, 2), 2))
                                 for topping in line.topping_ids_1
                                 | line.topping_ids_2 | line.topping_ids_3],
                    'quantity':
                    line.quantity,
                    'price':
                    line.price,
                    'state':
                    line.state,  # Only used for _get_state
                    'note':
                    line.note
                } for line in lines
            ]
            raw_state, state = self._get_state(lines)
            infos.update({
                'total':
                float_repr(
                    float_round(sum(line['price'] for line in lines), 2), 2),
                'raw_state':
                raw_state,
                'state':
                state,
                'lines':
                lines,
            })
        return infos
Esempio n. 10
0
def float_to_time(hours, moment='am', tz=None):
    """ Convert a number of hours into a time object. """
    if hours == 12.0 and moment == 'pm':
        return time.max
    fractional, integral = math.modf(hours)
    if moment == 'pm':
        integral += 12
    res = time(int(integral),
               int(float_round(60 * fractional, precision_digits=0)), 0)
    if tz:
        res = res.replace(tzinfo=pytz.timezone(tz))
    return res
Esempio n. 11
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_is_zero(0.0, precision_rounding=0.0)

        with self.assertRaises(AssertionError):
            float_is_zero(0.0, precision_rounding=-0.1)

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

        with self.assertRaises(AssertionError):
            float_compare(1.0, 1.0, precision_rounding=0.0)

        with self.assertRaises(AssertionError):
            float_compare(1.0, 1.0, precision_rounding=-0.1)

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

        with self.assertRaises(AssertionError):
            float_round(1.25, precision_rounding=0.0)

        with self.assertRaises(AssertionError):
            float_round(1.25, precision_rounding=-0.1)
Esempio n. 12
0
 def get_operations(self, bom_id=False, qty=0, level=0):
     bom = self.env['mrp.bom'].browse(bom_id)
     lines = self._get_operation_line(
         bom.routing_id,
         float_round(qty / bom.product_qty,
                     precision_rounding=1,
                     rounding_method='UP'), level)
     values = {
         'bom_id': bom_id,
         'currency': self.env.company.currency_id,
         'operations': lines,
     }
     return self.env.ref('mrp.report_mrp_operation_line').render(
         {'data': values})
Esempio n. 13
0
    def test_move_picking_with_package(self):
        """
        355.4 rounded with 0.001 precision is 355.40000000000003.
        check that nonetheless, moving a picking is accepted
        """
        self.assertEqual(self.productA.uom_id.rounding, 0.001)
        self.assertEqual(
            float_round(355.4, precision_rounding=self.productA.uom_id.rounding),
            355.40000000000003,
        )
        location_dict = {
            'location_id': self.stock_location.id,
        }
        quant = self.env['stock.quant'].create({
            **location_dict,
            **{'product_id': self.productA.id, 'quantity': 355.4},  # important number
        })
        package = self.env['stock.quant.package'].create({
            **location_dict, **{'quant_ids': [(6, 0, [quant.id])]},
        })
        location_dict.update({
            'state': 'draft',
            'location_dest_id': self.ship_location.id,
        })
        move = self.env['stock.move'].create({
            **location_dict,
            **{
                'name': "XXX",
                'product_id': self.productA.id,
                'product_uom': self.productA.uom_id.id,
                'product_uom_qty': 355.40000000000003,  # other number
            }})
        picking = self.env['stock.picking'].create({
            **location_dict,
            **{
                'picking_type_id': self.warehouse.in_type_id.id,
                'move_lines': [(6, 0, [move.id])],
        }})

        picking.action_confirm()
        picking.action_assign()
        move.quantity_done = move.reserved_availability
        picking.action_done()
Esempio n. 14
0
 def _compute_quantity(self, qty, to_unit, round=True, rounding_method='UP', raise_if_failure=True):
     """ Convert the given quantity from the current UoM `self` into a given one
         :param qty: the quantity to convert
         :param to_unit: the destination UoM record (uom.uom)
         :param raise_if_failure: only if the conversion is not possible
             - if true, raise an exception if the conversion is not possible (different UoM category),
             - otherwise, return the initial quantity
     """
     if not self:
         return qty
     self.ensure_one()
     if self.category_id.id != to_unit.category_id.id:
         if raise_if_failure:
             raise UserError(_('The unit of measure %s defined on the order line doesn\'t belong to the same category than the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.') % (self.name, to_unit.name))
         else:
             return qty
     amount = qty / self.factor
     if to_unit:
         amount = amount * to_unit.factor
         if round:
             amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
     return amount
Esempio n. 15
0
 def _get_operation_line(self, routing, qty, level):
     operations = []
     total = 0.0
     for operation in routing.operation_ids:
         operation_cycle = float_round(qty /
                                       operation.workcenter_id.capacity,
                                       precision_rounding=1,
                                       rounding_method='UP')
         duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
         total = ((duration_expected / 60.0) *
                  operation.workcenter_id.costs_hour)
         operations.append({
             'level':
             level or 0,
             'operation':
             operation,
             'name':
             operation.name + ' - ' + operation.workcenter_id.name,
             'duration_expected':
             duration_expected,
             'total':
             self.env.company.currency_id.round(total),
         })
     return operations
Esempio n. 16
0
 def _update_finished_move(self):
     """ Update the finished move & move lines in order to set the finished
     product lot on it as well as the produced quantity. This method get the
     information either from the last workorder or from the Produce wizard."""
     production_move = self.production_id.move_finished_ids.filtered(
         lambda move: move.product_id == self.product_id and move.state
         not in ('done', 'cancel'))
     if production_move and production_move.product_id.tracking != 'none':
         if not self.finished_lot_id:
             raise UserError(
                 _('You need to provide a lot for the finished product.'))
         move_line = production_move.move_line_ids.filtered(
             lambda line: line.lot_id.id == self.finished_lot_id.id)
         if move_line:
             if self.product_id.tracking == 'serial':
                 raise UserError(
                     _('You cannot produce the same serial number twice.'))
             move_line.product_uom_qty += self.qty_producing
             move_line.qty_done += self.qty_producing
         else:
             location_dest_id = production_move.location_dest_id._get_putaway_strategy(
                 self.product_id).id or production_move.location_dest_id.id
             move_line.create({
                 'move_id': production_move.id,
                 'product_id': production_move.product_id.id,
                 'lot_id': self.finished_lot_id.id,
                 'product_uom_qty': self.qty_producing,
                 'product_uom_id': self.product_uom_id.id,
                 'qty_done': self.qty_producing,
                 'location_id': production_move.location_id.id,
                 'location_dest_id': location_dest_id,
             })
     else:
         rounding = production_move.product_uom.rounding
         production_move._set_quantity_done(
             float_round(self.qty_producing, precision_rounding=rounding))
Esempio n. 17
0
 def _compute_qty_remaining(self):
     for wo in self:
         wo.qty_remaining = float_round(
             wo.qty_production - wo.qty_produced,
             precision_rounding=wo.production_id.product_uom_id.rounding)
Esempio n. 18
0
    def compute_landed_cost(self):
        AdjustementLines = self.env['stock.valuation.adjustment.lines']
        AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink()

        digits = self.env['decimal.precision'].precision_get('Product Price')
        towrite_dict = {}
        for cost in self.filtered(lambda cost: cost.picking_ids):
            total_qty = 0.0
            total_cost = 0.0
            total_weight = 0.0
            total_volume = 0.0
            total_line = 0.0
            all_val_line_values = cost.get_valuation_lines()
            for val_line_values in all_val_line_values:
                for cost_line in cost.cost_lines:
                    val_line_values.update({
                        'cost_id': cost.id,
                        'cost_line_id': cost_line.id
                    })
                    self.env['stock.valuation.adjustment.lines'].create(
                        val_line_values)
                total_qty += val_line_values.get('quantity', 0.0)
                total_weight += val_line_values.get('weight', 0.0)
                total_volume += val_line_values.get('volume', 0.0)

                former_cost = val_line_values.get('former_cost', 0.0)
                # round this because former_cost on the valuation lines is also rounded
                total_cost += tools.float_round(
                    former_cost,
                    precision_digits=digits) if digits else former_cost

                total_line += 1

            for line in cost.cost_lines:
                value_split = 0.0
                for valuation in cost.valuation_adjustment_lines:
                    value = 0.0
                    if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
                        if line.split_method == 'by_quantity' and total_qty:
                            per_unit = (line.price_unit / total_qty)
                            value = valuation.quantity * per_unit
                        elif line.split_method == 'by_weight' and total_weight:
                            per_unit = (line.price_unit / total_weight)
                            value = valuation.weight * per_unit
                        elif line.split_method == 'by_volume' and total_volume:
                            per_unit = (line.price_unit / total_volume)
                            value = valuation.volume * per_unit
                        elif line.split_method == 'equal':
                            value = (line.price_unit / total_line)
                        elif line.split_method == 'by_current_cost_price' and total_cost:
                            per_unit = (line.price_unit / total_cost)
                            value = valuation.former_cost * per_unit
                        else:
                            value = (line.price_unit / total_line)

                        if digits:
                            value = tools.float_round(value,
                                                      precision_digits=digits,
                                                      rounding_method='UP')
                            fnc = min if line.price_unit > 0 else max
                            value = fnc(value, line.price_unit - value_split)
                            value_split += value

                        if valuation.id not in towrite_dict:
                            towrite_dict[valuation.id] = value
                        else:
                            towrite_dict[valuation.id] += value
        for key, value in towrite_dict.items():
            AdjustementLines.browse(key).write(
                {'additional_landed_cost': value})
        return True
Esempio n. 19
0
def time_to_float(t):
    return float_round(t.hour + t.minute / 60 + t.second / 3600,
                       precision_digits=2)
Esempio n. 20
0
    def test_20_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')
Esempio n. 21
0
 def _get_bom(self,
              bom_id=False,
              product_id=False,
              line_qty=False,
              line_id=False,
              level=False):
     bom = self.env['mrp.bom'].browse(bom_id)
     bom_quantity = line_qty
     if line_id:
         current_line = self.env['mrp.bom.line'].browse(int(line_id))
         bom_quantity = current_line.product_uom_id._compute_quantity(
             line_qty, bom.product_uom_id)
     # Display bom components for current selected product variant
     if product_id:
         product = self.env['product.product'].browse(int(product_id))
     else:
         product = bom.product_id or bom.product_tmpl_id.product_variant_id
     if product:
         attachments = self.env['mrp.document'].search([
             '|', '&', ('res_model', '=', 'product.product'),
             ('res_id', '=', product.id), '&',
             ('res_model', '=', 'product.template'),
             ('res_id', '=', product.product_tmpl_id.id)
         ])
     else:
         product = bom.product_tmpl_id
         attachments = self.env['mrp.document'].search([
             ('res_model', '=', 'product.template'),
             ('res_id', '=', product.id)
         ])
     operations = []
     if bom.product_qty > 0:
         operations = self._get_operation_line(
             bom.routing_id,
             float_round(bom_quantity / bom.product_qty,
                         precision_rounding=1,
                         rounding_method='UP'), 0)
     company = bom.company_id or self.env.company
     lines = {
         'bom':
         bom,
         'bom_qty':
         bom_quantity,
         'bom_prod_name':
         product.display_name,
         'currency':
         company.currency_id,
         'product':
         product,
         'code':
         bom and bom.display_name or '',
         'price':
         product.uom_id._compute_price(
             product.with_context(force_company=company.id).standard_price,
             bom.product_uom_id) * bom_quantity,
         'total':
         sum([op['total'] for op in operations]),
         'level':
         level or 0,
         'operations':
         operations,
         'operations_cost':
         sum([op['total'] for op in operations]),
         'attachments':
         attachments,
         'operations_time':
         sum([op['duration_expected'] for op in operations])
     }
     components, total = self._get_bom_lines(bom, bom_quantity, product,
                                             line_id, level)
     lines['components'] = components
     lines['total'] += total
     return lines
Esempio n. 22
0
    def explode(self, product, quantity, picking_type=False):
        """
            Explodes the BoM and creates two lists with all the information you need: bom_done and line_done
            Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
            and converted into its UoM
        """
        from collections import defaultdict

        graph = defaultdict(list)
        V = set()

        def check_cycle(v, visited, recStack, graph):
            visited[v] = True
            recStack[v] = True
            for neighbour in graph[v]:
                if visited[neighbour] == False:
                    if check_cycle(neighbour, visited, recStack,
                                   graph) == True:
                        return True
                elif recStack[neighbour] == True:
                    return True
            recStack[v] = False
            return False

        boms_done = [(self, {
            'qty': quantity,
            'product': product,
            'original_qty': quantity,
            'parent_line': False
        })]
        lines_done = []
        V |= set([product.product_tmpl_id.id])

        bom_lines = [(bom_line, product, quantity, False)
                     for bom_line in self.bom_line_ids]
        for bom_line in self.bom_line_ids:
            V |= set([bom_line.product_id.product_tmpl_id.id])
            graph[product.product_tmpl_id.id].append(
                bom_line.product_id.product_tmpl_id.id)
        while bom_lines:
            current_line, current_product, current_qty, parent_line = bom_lines[
                0]
            bom_lines = bom_lines[1:]

            if current_line._skip_bom_line(current_product):
                continue

            line_quantity = current_qty * current_line.product_qty
            bom = self._bom_find(product=current_line.product_id,
                                 picking_type=picking_type
                                 or self.picking_type_id,
                                 company_id=self.company_id.id,
                                 bom_type='phantom')
            if bom:
                converted_line_quantity = current_line.product_uom_id._compute_quantity(
                    line_quantity / bom.product_qty, bom.product_uom_id)
                bom_lines = [(line, current_line.product_id,
                              converted_line_quantity, current_line)
                             for line in bom.bom_line_ids] + bom_lines
                for bom_line in bom.bom_line_ids:
                    graph[current_line.product_id.product_tmpl_id.id].append(
                        bom_line.product_id.product_tmpl_id.id)
                    if bom_line.product_id.product_tmpl_id.id in V and check_cycle(
                            bom_line.product_id.product_tmpl_id.id,
                        {key: False
                         for key in V}, {key: False
                                         for key in V}, graph):
                        raise UserError(
                            _('Recursion error!  A product with a Bill of Material should not have itself in its BoM or child BoMs!'
                              ))
                    V |= set([bom_line.product_id.product_tmpl_id.id])
                boms_done.append((bom, {
                    'qty': converted_line_quantity,
                    'product': current_product,
                    'original_qty': quantity,
                    'parent_line': current_line
                }))
            else:
                # We round up here because the user expects that if he has to consume a little more, the whole UOM unit
                # should be consumed.
                rounding = current_line.product_uom_id.rounding
                line_quantity = float_round(line_quantity,
                                            precision_rounding=rounding,
                                            rounding_method='UP')
                lines_done.append((current_line, {
                    'qty': line_quantity,
                    'product': current_product,
                    'original_qty': quantity,
                    'parent_line': parent_line
                }))

        return boms_done, lines_done
Esempio n. 23
0
    def change_prod_qty(self):
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        for wizard in self:
            production = wizard.mo_id
            produced = sum(
                production.move_finished_ids.filtered(
                    lambda m: m.product_id == production.product_id).mapped(
                        'quantity_done'))
            if wizard.product_qty < produced:
                format_qty = '%.{precision}f'.format(precision=precision)
                raise UserError(
                    _("You have already processed %s. Please input a quantity higher than %s "
                      ) % (format_qty % produced, format_qty % produced))
            old_production_qty = production.product_qty
            production.write({'product_qty': wizard.product_qty})
            done_moves = production.move_finished_ids.filtered(
                lambda x: x.state == 'done' and x.product_id == production.
                product_id)
            qty_produced = production.product_id.uom_id._compute_quantity(
                sum(done_moves.mapped('product_qty')),
                production.product_uom_id)
            factor = production.product_uom_id._compute_quantity(
                production.product_qty - qty_produced, production.bom_id.
                product_uom_id) / production.bom_id.product_qty
            boms, lines = production.bom_id.explode(
                production.product_id,
                factor,
                picking_type=production.bom_id.picking_type_id)
            documents = {}
            for line, line_data in lines:
                if line.child_bom_id and line.child_bom_id.type == 'phantom' or\
                        line.product_id.type not in ['product', 'consu']:
                    continue
                move = production.move_raw_ids.filtered(
                    lambda x: x.bom_line_id.id == line.id and x.state not in
                    ('done', 'cancel'))
                if move:
                    move = move[0]
                    old_qty = move.product_uom_qty
                else:
                    old_qty = 0
                iterate_key = production._get_document_iterate_key(move)
                if iterate_key:
                    document = self.env[
                        'stock.picking']._log_activity_get_documents(
                            {move: (line_data['qty'], old_qty)}, iterate_key,
                            'UP')
                    for key, value in document.items():
                        if documents.get(key):
                            documents[key] += [value]
                        else:
                            documents[key] = [value]

                production._update_raw_move(line, line_data)

            production._log_manufacture_exception(documents)
            operation_bom_qty = {}
            for bom, bom_data in boms:
                for operation in bom.routing_id.operation_ids:
                    operation_bom_qty[operation.id] = bom_data['qty']
            finished_moves_modification = self._update_finished_moves(
                production, production.product_qty - qty_produced,
                old_production_qty)
            production._log_downside_manufactured_quantity(
                finished_moves_modification)
            moves = production.move_raw_ids.filtered(lambda x: x.state not in
                                                     ('done', 'cancel'))
            moves._action_assign()
            for wo in production.workorder_ids:
                operation = wo.operation_id
                if operation_bom_qty.get(operation.id):
                    cycle_number = float_round(
                        operation_bom_qty[operation.id] /
                        operation.workcenter_id.capacity,
                        precision_digits=0,
                        rounding_method='UP')
                    wo.duration_expected = (
                        operation.workcenter_id.time_start +
                        operation.workcenter_id.time_stop +
                        cycle_number * operation.time_cycle * 100.0 /
                        operation.workcenter_id.time_efficiency)
                quantity = wo.qty_production - wo.qty_produced
                if production.product_id.tracking == 'serial':
                    quantity = 1.0 if not float_is_zero(
                        quantity, precision_digits=precision) else 0.0
                else:
                    quantity = quantity if (quantity > 0) else 0
                if float_is_zero(quantity, precision_digits=precision):
                    wo.finished_lot_id = False
                    wo._workorder_line_ids().unlink()
                wo.qty_producing = quantity
                if wo.qty_produced < wo.qty_production and wo.state == 'done':
                    wo.state = 'progress'
                if wo.qty_produced == wo.qty_production and wo.state == 'progress':
                    wo.state = 'done'
                    if wo.next_work_order_id.state == 'pending':
                        wo.next_work_order_id.state = 'ready'
                # assign moves; last operation receive all unassigned moves
                # TODO: following could be put in a function as it is similar as code in _workorders_create
                # TODO: only needed when creating new moves
                moves_raw = production.move_raw_ids.filtered(
                    lambda move: move.operation_id == operation and move.state
                    not in ('done', 'cancel'))
                if wo == production.workorder_ids[-1]:
                    moves_raw |= production.move_raw_ids.filtered(
                        lambda move: not move.operation_id)
                moves_finished = production.move_finished_ids.filtered(
                    lambda move: move.operation_id == operation
                )  #TODO: code does nothing, unless maybe by_products?
                moves_raw.mapped('move_line_ids').write(
                    {'workorder_id': wo.id})
                (moves_finished + moves_raw).write({'workorder_id': wo.id})
                if wo.state not in ('done', 'cancel'):
                    line_values = wo._update_workorder_lines()
                    wo._workorder_line_ids().create(line_values['to_create'])
                    if line_values['to_delete']:
                        line_values['to_delete'].unlink()
                    for line, vals in line_values['to_update'].items():
                        line.write(vals)
        return {}
Esempio n. 24
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)
        return line_values
Esempio n. 25
0
    def _compute_price_rule(self,
                            products_qty_partner,
                            date=False,
                            uom_id=False):
        """ Low-level method - Mono pricelist, multi products
        Returns: dict{product_id: (price, suitable_rule) for the given pricelist}

        Date in context can be a date, datetime, ...

            :param products_qty_partner: list of typles products, quantity, partner
            :param datetime date: validity date
            :param ID uom_id: intermediate unit of measure
        """
        self.ensure_one()
        if not date:
            date = self._context.get('date') or fields.Date.today()
        date = fields.Date.to_date(
            date)  # boundary conditions differ if we have a datetime
        if not uom_id and self._context.get('uom'):
            uom_id = self._context['uom']
        if uom_id:
            # rebrowse with uom if given
            products = [
                item[0].with_context(uom=uom_id)
                for item in products_qty_partner
            ]
            products_qty_partner = [
                (products[index], data_struct[1], data_struct[2])
                for index, data_struct in enumerate(products_qty_partner)
            ]
        else:
            products = [item[0] for item in products_qty_partner]

        if not products:
            return {}

        categ_ids = {}
        for p in products:
            categ = p.categ_id
            while categ:
                categ_ids[categ.id] = True
                categ = categ.parent_id
        categ_ids = list(categ_ids)

        is_product_template = products[0]._name == "product.template"
        if is_product_template:
            prod_tmpl_ids = [tmpl.id for tmpl in products]
            # all variants of all products
            prod_ids = [
                p.id for p in list(
                    chain.from_iterable(
                        [t.product_variant_ids for t in products]))
            ]
        else:
            prod_ids = [product.id for product in products]
            prod_tmpl_ids = [
                product.product_tmpl_id.id for product in products
            ]

        items = self._compute_price_rule_get_items(products_qty_partner, date,
                                                   uom_id, prod_tmpl_ids,
                                                   prod_ids, categ_ids)

        results = {}
        for product, qty, partner in products_qty_partner:
            results[product.id] = 0.0
            suitable_rule = False

            # Final unit price is computed according to `qty` in the `qty_uom_id` UoM.
            # An intermediary unit price may be computed according to a different UoM, in
            # which case the price_uom_id contains that UoM.
            # The final price will be converted to match `qty_uom_id`.
            qty_uom_id = self._context.get('uom') or product.uom_id.id
            price_uom_id = product.uom_id.id
            qty_in_product_uom = qty
            if qty_uom_id != product.uom_id.id:
                try:
                    qty_in_product_uom = self.env['uom.uom'].browse([
                        self._context['uom']
                    ])._compute_quantity(qty, product.uom_id)
                except UserError:
                    # Ignored - incompatible UoM in context, use default product UoM
                    pass

            # if Public user try to access standard price from website sale, need to call price_compute.
            # TDE SURPRISE: product can actually be a template
            price = product.price_compute('list_price')[product.id]

            price_uom = self.env['uom.uom'].browse([qty_uom_id])
            for rule in items:
                if rule.min_quantity and qty_in_product_uom < rule.min_quantity:
                    continue
                if is_product_template:
                    if rule.product_tmpl_id and product.id != rule.product_tmpl_id.id:
                        continue
                    if rule.product_id and not (
                            product.product_variant_count == 1
                            and product.product_variant_id.id
                            == rule.product_id.id):
                        # product rule acceptable on template if has only one variant
                        continue
                else:
                    if rule.product_tmpl_id and product.product_tmpl_id.id != rule.product_tmpl_id.id:
                        continue
                    if rule.product_id and product.id != rule.product_id.id:
                        continue

                if rule.categ_id:
                    cat = product.categ_id
                    while cat:
                        if cat.id == rule.categ_id.id:
                            break
                        cat = cat.parent_id
                    if not cat:
                        continue

                if rule.base == 'pricelist' and rule.base_pricelist_id:
                    price_tmp = rule.base_pricelist_id._compute_price_rule(
                        [(product, qty, partner)], date,
                        uom_id)[product.id][0]  # TDE: 0 = price, 1 = rule
                    price = rule.base_pricelist_id.currency_id._convert(
                        price_tmp,
                        self.currency_id,
                        self.env.company,
                        date,
                        round=False)
                else:
                    # if base option is public price take sale price else cost price of product
                    # price_compute returns the price in the context UoM, i.e. qty_uom_id
                    price = product.price_compute(rule.base)[product.id]

                convert_to_price_uom = (lambda price: product.uom_id.
                                        _compute_price(price, price_uom))

                if price is not False:
                    if rule.compute_price == 'fixed':
                        price = convert_to_price_uom(rule.fixed_price)
                    elif rule.compute_price == 'percentage':
                        price = (price - (price *
                                          (rule.percent_price / 100))) or 0.0
                    else:
                        # complete formula
                        price_limit = price
                        price = (price - (price *
                                          (rule.price_discount / 100))) or 0.0
                        if rule.price_round:
                            price = tools.float_round(
                                price, precision_rounding=rule.price_round)

                        if rule.price_surcharge:
                            price_surcharge = convert_to_price_uom(
                                rule.price_surcharge)
                            price += price_surcharge

                        if rule.price_min_margin:
                            price_min_margin = convert_to_price_uom(
                                rule.price_min_margin)
                            price = max(price, price_limit + price_min_margin)

                        if rule.price_max_margin:
                            price_max_margin = convert_to_price_uom(
                                rule.price_max_margin)
                            price = min(price, price_limit + price_max_margin)
                    suitable_rule = rule
                break
            # Final price conversion into pricelist currency
            if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist':
                if suitable_rule.base == 'standard_price':
                    cur = product.cost_currency_id
                else:
                    cur = product.currency_id
                price = cur._convert(price,
                                     self.currency_id,
                                     self.env.company,
                                     date,
                                     round=False)

            if not suitable_rule:
                cur = product.currency_id
                price = cur._convert(price,
                                     self.currency_id,
                                     self.env.company,
                                     date,
                                     round=False)

            results[product.id] = (price, suitable_rule and suitable_rule.id
                                   or False)

        return results
Esempio n. 26
0
    def _plan_prepare_values(self):
        currency = self.env.company.currency_id
        uom_hour = self.env.ref('uom.product_uom_hour')
        hour_rounding = uom_hour.rounding
        billable_types = [
            'non_billable', 'non_billable_project', 'billable_time',
            'billable_fixed'
        ]

        values = {
            'projects': self,
            'currency': currency,
            'timesheet_domain': [('project_id', 'in', self.ids)],
            'profitability_domain': [('project_id', 'in', self.ids)],
            'stat_buttons': self._plan_get_stat_button(),
        }

        #
        # Hours, Rates and Profitability
        #
        dashboard_values = {
            'hours': dict.fromkeys(billable_types + ['total'], 0.0),
            'rates': dict.fromkeys(billable_types + ['total'], 0.0),
            'profit': {
                'invoiced': 0.0,
                'to_invoice': 0.0,
                'cost': 0.0,
                'total': 0.0,
            }
        }

        # hours from non-invoiced timesheets that are linked to canceled so
        canceled_hours_domain = [('project_id', 'in', self.ids),
                                 ('timesheet_invoice_type', '!=', False),
                                 ('so_line.state', '=', 'cancel')]
        total_canceled_hours = sum(self.env['account.analytic.line'].search(
            canceled_hours_domain).mapped('unit_amount'))
        dashboard_values['hours']['canceled'] = float_round(
            total_canceled_hours, precision_rounding=hour_rounding)
        dashboard_values['hours']['total'] += float_round(
            total_canceled_hours, precision_rounding=hour_rounding)

        # hours (from timesheet) and rates (by billable type)
        dashboard_domain = [('project_id', 'in', self.ids),
                            ('timesheet_invoice_type', '!=', False), '|',
                            ('so_line', '=', False),
                            ('so_line.state', '!=', 'cancel')
                            ]  # force billable type
        dashboard_data = self.env['account.analytic.line'].read_group(
            dashboard_domain, ['unit_amount', 'timesheet_invoice_type'],
            ['timesheet_invoice_type'])
        dashboard_total_hours = sum(
            [data['unit_amount']
             for data in dashboard_data]) + total_canceled_hours
        for data in dashboard_data:
            billable_type = data['timesheet_invoice_type']
            dashboard_values['hours'][billable_type] = float_round(
                data.get('unit_amount'), precision_rounding=hour_rounding)
            dashboard_values['hours']['total'] += float_round(
                data.get('unit_amount'), precision_rounding=hour_rounding)
            # rates
            rate = round(
                data.get('unit_amount') / dashboard_total_hours *
                100, 2) if dashboard_total_hours else 0.0
            dashboard_values['rates'][billable_type] = rate
            dashboard_values['rates']['total'] += rate

        # rates from non-invoiced timesheets that are linked to canceled so
        dashboard_values['rates']['canceled'] = float_round(
            100 * total_canceled_hours / (dashboard_total_hours or 1),
            precision_rounding=hour_rounding)

        # profitability, using profitability SQL report
        profit = dict.fromkeys([
            'invoiced', 'to_invoice', 'cost', 'expense_cost',
            'expense_amount_untaxed_invoiced', 'total'
        ], 0.0)
        profitability_raw_data = self.env[
            'project.profitability.report'].read_group(
                [('project_id', 'in', self.ids)], [
                    'project_id', 'amount_untaxed_to_invoice',
                    'amount_untaxed_invoiced', 'timesheet_cost',
                    'expense_cost', 'expense_amount_untaxed_invoiced'
                ], ['project_id'])
        for data in profitability_raw_data:
            profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0)
            profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0)
            profit['cost'] += data.get('timesheet_cost', 0.0)
            profit['expense_cost'] += data.get('expense_cost', 0.0)
            profit['expense_amount_untaxed_invoiced'] += data.get(
                'expense_amount_untaxed_invoiced', 0.0)
        profit['total'] = sum([profit[item] for item in profit.keys()])
        dashboard_values['profit'] = profit

        values['dashboard'] = dashboard_values

        #
        # Time Repartition (per employee per billable types)
        #
        user_ids = self.env['project.task'].sudo().read_group(
            [('project_id', 'in', self.ids),
             ('user_id', '!=', False)], ['user_id'], ['user_id'])
        user_ids = [user_id['user_id'][0] for user_id in user_ids]
        employee_ids = self.env['res.users'].sudo().search_read(
            [('id', 'in', user_ids)], ['employee_ids'])
        # flatten the list of list
        employee_ids = list(
            itertools.chain.from_iterable(
                [employee_id['employee_ids'] for employee_id in employee_ids]))

        aal_employee_ids = self.env['account.analytic.line'].read_group(
            [('project_id', 'in', self.ids),
             ('employee_id', '!=', False)], ['employee_id'], ['employee_id'])
        employee_ids.extend(
            list(map(lambda x: x['employee_id'][0], aal_employee_ids)))

        employees = self.env['hr.employee'].sudo().browse(employee_ids)
        repartition_domain = [('project_id', 'in', self.ids),
                              ('employee_id', '!=', False),
                              ('timesheet_invoice_type', '!=', False)
                              ]  # force billable type
        # repartition data, without timesheet on cancelled so
        repartition_data = self.env['account.analytic.line'].read_group(
            repartition_domain +
            ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')],
            ['employee_id', 'timesheet_invoice_type', 'unit_amount'],
            ['employee_id', 'timesheet_invoice_type'],
            lazy=False)
        # read timesheet on cancelled so
        cancelled_so_timesheet = self.env['account.analytic.line'].read_group(
            repartition_domain + [('so_line.state', '=', 'cancel')],
            ['employee_id', 'unit_amount'], ['employee_id'],
            lazy=False)
        repartition_data += [{
            **canceled, 'timesheet_invoice_type': 'canceled'
        } for canceled in cancelled_so_timesheet]

        # set repartition per type per employee
        repartition_employee = {}
        for employee in employees:
            repartition_employee[employee.id] = dict(
                employee_id=employee.id,
                employee_name=employee.name,
                non_billable_project=0.0,
                non_billable=0.0,
                billable_time=0.0,
                billable_fixed=0.0,
                canceled=0.0,
                total=0.0,
            )
        for data in repartition_data:
            employee_id = data['employee_id'][0]
            repartition_employee.setdefault(
                employee_id,
                dict(
                    employee_id=data['employee_id'][0],
                    employee_name=data['employee_id'][1],
                    non_billable_project=0.0,
                    non_billable=0.0,
                    billable_time=0.0,
                    billable_fixed=0.0,
                    canceled=0.0,
                    total=0.0,
                ))[data['timesheet_invoice_type']] = float_round(
                    data.get('unit_amount', 0.0),
                    precision_rounding=hour_rounding)
            repartition_employee[employee_id][
                '__domain_' +
                data['timesheet_invoice_type']] = data['__domain']
        # compute total
        for employee_id, vals in repartition_employee.items():
            repartition_employee[employee_id]['total'] = sum(
                [vals[inv_type] for inv_type in [*billable_types, 'canceled']])
        hours_per_employee = [
            repartition_employee[employee_id]['total']
            for employee_id in repartition_employee
        ]
        values['repartition_employee_max'] = (max(hours_per_employee) if
                                              hours_per_employee else 1) or 1
        values['repartition_employee'] = repartition_employee

        #
        # Table grouped by SO / SOL / Employees
        #
        timesheet_forecast_table_rows = self._table_get_line_values()
        if timesheet_forecast_table_rows:
            values['timesheet_forecast_table'] = timesheet_forecast_table_rows
        return values
Esempio n. 27
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.company.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_res in location_data.items():
                location_orderpoints = location_res['orderpoints']
                product_context = dict(self._context, location=location_orderpoints[0].location_id.id)
                substract_quantity = location_orderpoints._quantity_in_progress()

                for group in location_res['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_res['products'].with_context(product_context)._product_available()
                    for orderpoint in location_orderpoints:
                        try:
                            op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available']
                            if op_product_virtual is None:
                                continue
                            if float_compare(op_product_virtual, orderpoint.product_min_qty, precision_rounding=orderpoint.product_uom.rounding) <= 0:
                                qty = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - op_product_virtual
                                remainder = orderpoint.qty_multiple > 0 and qty % orderpoint.qty_multiple or 0.0

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

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

                                qty -= substract_quantity[orderpoint.id]
                                qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding)
                                if qty_rounded > 0:
                                    values = orderpoint._prepare_procurement_values(qty_rounded, **group['procurement_values'])
                                    try:
                                        with self._cr.savepoint():
                                            #TODO: make it batch
                                            self.env['procurement.group'].run([self.env['procurement.group'].Procurement(
                                                orderpoint.product_id, qty_rounded, orderpoint.product_uom,
                                                orderpoint.location_id, orderpoint.name, orderpoint.name,
                                                orderpoint.company_id, 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 {}