<commit_before># -*- coding: utf-8 -*-

##############################################################################
#
# Sale - Product Variant Selection Assistant
# Copyright (C) 2016 OpusVL (<http://opusvl.com/>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

from openerp.tests import common
from openerp.exceptions import ValidationError

class AssistantTestCommon(common.TransactionCase):
    def setUp(self):
        super(AssistantTestCommon, self).setUp()
        Partner = self.env['res.partner']
        ProductTemplate = self.env['product.template']
        ProductProduct = self.env['product.product']
        SaleOrder = self.env['sale.order']
        self.partners = {}
        self.products = {}
        self.attributes = {}
        self.attr_values = {}
        self.orders = {}

        self.partners['acme'] = Partner.create(dict(
            name='TEST ACME',
            is_company=True,
            customer=True,
            supplier=False,
        ))
        self.setup_add_attribute('legs', ['Pedestal', 'Four Straight', 'Three Straight'])
        self.setup_add_attribute('maturity', ['Mild', 'Medium', 'Mature', 'Extra Mature'])
        self.setup_add_attribute('wood', ['Mahogany', 'Teak', 'MDF', 'Balsa', 'Oak'])
        self.products['table'] = ProductTemplate.create(dict(
            name='TEST Table',
            attribute_line_ids=[
                (0, False, dict(
                    attribute_id=self.attributes['legs'].id,
                    value_ids=[
                        (6, False, [v.id for v in self.attr_values['legs'].values()]),
                    ],
                )),
                (0, False, dict(
                    attribute_id=self.attributes['wood'].id,
                    value_ids=[
                        (6, False, [self.attr_values['wood'][n].id for n in ['Mahogany', 'Teak']]),
                    ],
                )),
            ],
        ))
        self.products['cheese'] = ProductTemplate.create(dict(
            name='Cheese',
            attribute_line_ids=[
                (0, False, dict(
                    attribute_id=self.attributes['maturity'].id,
                    value_ids=[
                        (6, False, [self.attr_values['maturity'][n].id for n in ['Mature', 'Extra Mature']]),
                    ]
                ))
            ],
        ))
        self.orders['FIRST'] = SaleOrder.create(dict(
            partner_id = self.partners['acme'].id,
        ))

    def setup_add_attribute(self, name, value_names):
        ProductAttribute = self.env['product.attribute']
        ProductAttributeValue = self.env['product.attribute.value']
        self.attributes[name] = attr = ProductAttribute.create(dict(name='TEST '+name))
        self.attr_values[name] = values = {}
        for val in value_names:
            values[val] = ProductAttributeValue.create(dict(name=val, attribute_id=attr.id))

    def create_line(self, order_name, template_name=False):
        order = self.orders[order_name]
        return self.env['sale.order.line'].create(dict(
            order_id=order.id,
            name='NOT NULL',
            variant_assistant_product_template_id=template_name and self.products[template_name].id,
        ))

    def _select_attribute(self, line, attr_name, value_name):
        line.write({
            'variant_assistant_attribute_choice_ids': [(0, False, {
                'attribute_id': self.attributes[attr_name].id,
                'value_id': self.attr_values[attr_name][value_name].id,
            })],
        })

class TestHelperTests(AssistantTestCommon):
    at_install = False
    post_install = True

    def test_create_line_with_template(self):
        result = self.create_line('FIRST', template_name='cheese')

        self.assertEqual(result.order_id, self.orders['FIRST'])
        self.assertEqual(result.variant_assistant_product_template_id, self.products['cheese'])
        self.assertEqual(len(result.variant_assistant_attribute_choice_ids), 0)

    def test_create_line_without_template(self):
        result = self.create_line('FIRST')

        self.assertEqual(result.order_id, self.orders['FIRST'])
        self.assertFalse(result.variant_assistant_product_template_id)

    def test_select_attribute_from_empty(self):
        line = self.create_line(order_name='FIRST', template_name='table')
        self._select_attribute(line, 'legs', 'Pedestal')

        result = line.variant_assistant_attribute_choice_ids

        self.assertEqual(len(result), 1)
        self.assertEqual(result[0].attribute_id, self.attributes['legs'])
        self.assertEqual(result[0].value_id, self.attr_values['legs']['Pedestal'])

class ResolverTests(AssistantTestCommon):
    """Test the logic that resolves the choices to a unique variant.
    """
    at_install = False
    post_install = True

    def test_nothing_with_no_template_selected(self):
        """Resolve to False if no template is selected.
        """
        line = self.create_line(order_name='FIRST', template_name=False)

        result = line._assistant_resolve_variant()

        self.assertFalse(result)

    def test_nothing_with_no_values_selected(self):
        line = self.create_line(order_name='FIRST', template_name='table')

        result = line._assistant_resolve_variant()

        self.assertFalse(result)

    def test_nothing_with_incomplete_values_selected(self):
        """Incomplete form produces falsey.
        """
        line = self.create_line(order_name='FIRST', template_name='table')
        self._select_attribute(line, 'legs', 'Pedestal')

        result = line._assistant_resolve_variant()

        self.assertFalse(result)

    def test_single_variant_with_complete_set_of_values(self):
        line = self.create_line(order_name='FIRST', template_name='table')
        self._select_attribute(line, 'legs', 'Pedestal')
        self._select_attribute(line, 'wood', 'Teak')

        result = line._assistant_resolve_variant()

        self.assertEqual(len(result), 1, 'Should select exactly one variant')
        self.assertSetEqual(frozenset(result.attribute_value_ids.mapped('name')), frozenset(['Teak', 'Pedestal']),
                            "Chosen variant has values Legs and Pedestal")

class AvailableValuesTests(AssistantTestCommon):
    """Test the available values method: _assistant_available_values()
    """
    at_install = False
    post_install = True

    def test_cheese_maturities(self):
        """cheese._assistant_available_values(maturity) -> Mature, Extra Mature
        """
        line = self.create_line('FIRST', 'cheese')

        result = line._assistant_available_values(self.attributes['maturity'])

        self.assertTrue(result)
        self.assertSetEqual(frozenset(result.mapped('name')), frozenset(['Mature', 'Extra Mature']))

    def test_cheese_legs(self):
        """cheese._assistant_available_values(legs) empty
        """
        line = self.create_line('FIRST', 'cheese')

        result = line._assistant_available_values(self.attributes['legs'])

        self.assertFalse(result, 'returned recordset should be empty')

class AvailableAttributesTests(AssistantTestCommon):
    """Test the available attributes method: _assistant_attributes()
    """
    at_install = False
    post_install = True

    def test_assistant_clear_by_default(self):
        order = self.orders['FIRST']

        line = self.env['sale.order.line'].create(dict(
            order_id=order.id,
            name='NOT NULL',
        ))

        self.assertFalse(line.variant_assistant_product_template_id)
        self.assertFalse(line.variant_assistant_attribute_choice_ids)

    def test_cheese_attributes(self):
        """_assistant_attributes() yields correct attributes for cheese
        """
        line = self.env['sale.order.line'].create(dict(
            order_id=self.orders['FIRST'].id,
            name='NOT NULL',
            variant_assistant_product_template_id=self.products['cheese'].id,
        ))

        result = line._assistant_attributes()

        self.assertSetEqual(frozenset(result.mapped('name')), frozenset(['TEST maturity']),
                            "Maturity is the only valid attribute of Cheese")

    def test_table_attributes(self):
        """_assistant_attributes() yields correct attributes for table
        """
        line = self.env['sale.order.line'].create(dict(
            order_id=self.orders['FIRST'].id,
            name='NOT NULL',
            variant_assistant_product_template_id=self.products['table'].id,
        ))

        result = line._assistant_attributes()

        self.assertSetEqual(frozenset(result.mapped('name')), frozenset(['TEST legs', 'TEST wood']))

    def test_blank_attributes(self):
        """_assistant_attributes() yields empty set if template not set.
        """
        line = self.env['sale.order.line'].create(dict(
            order_id=self.orders['FIRST'].id,
            name='NOT NULL',
        ))

        result = line._assistant_attributes()

        self.assertEqual(result, self.env['product.attribute'])

class ValidationTests(AssistantTestCommon):
    """Test validation of variant choice assistant models.

    The validation is there to help catch bugs - onupdate methods will be dealing with this.
    """
    at_install = False
    post_install = True

    def test_cheese_with_legs_option(self):
        with self.assertRaisesRegexp(ValidationError, r"Attribute must match product template"):
            cheese = self.products['cheese']
            self.orders['FIRST'].write(dict(
                order_line=[(0, False, dict(
                    name='NOT NULL',
                    variant_assistant_product_template_id=cheese.id,
                    variant_assistant_attribute_choice_ids=[
                        (0, False, dict(attribute_id=self.attributes['legs'].id)),
                    ],
                ))],
            ))

    def test_table_with_mature_legs(self):
        with self.assertRaisesRegexp(ValidationError, r"Value must match attribute"):
            table = self.products['table']
            legs = self.attributes['legs']
            mature = self.attr_values['maturity']['Mature']
            self.orders['FIRST'].write(dict(
                order_line=[(0, False, dict(
                    name='NOT NULL',
                    variant_assistant_product_template_id=table.id,
                    variant_assistant_attribute_choice_ids=[
                        (0, False, dict(attribute_id=legs.id,
                                        value_id=mature.id)),
                    ],
                ))],
            ))

    def test_mild_cheese(self):
        """Picking mild cheese raises ValidationError.

        This is because Mild, although a valid Maturity, is not valid for this cheese.
        """
        with self.assertRaisesRegexp(ValidationError, r"Value must match product"):
            cheese = self.products['cheese']
            maturity = self.attributes['maturity']
            mild = self.attr_values['maturity']['Mild']
            self.orders['FIRST'].write(dict(
                order_line=[(0, False, dict(
                    name='NOT NULL',
                    variant_assistant_product_template_id=cheese.id,
                    variant_assistant_attribute_choice_ids=[
                        (0, False, dict(attribute_id=maturity.id, value_id=mild.id)),
                    ],
                ))]
            ))

    def test_assistant_is_optional(self):
        """It is still OK to create an order line without filling in the assistant fields.
        """
        self.orders['FIRST'].write(dict(
            order_line=[(0, False, dict(product_id=self.products['table'].product_variant_ids[0].id))],
        ))

    def test_table_with_pedestal_legs(self):
        """It is OK to choose mature cheese.
        """
        table = self.products['table']
        legs = self.attributes['legs']
        pedestal = self.attr_values['legs']['Pedestal']
        self.orders['FIRST'].write(dict(
            order_line=[(0, False, dict(
                name='NOT NULL',   # This won't matter while we're doing the onupdate
                variant_assistant_product_template_id=table.id,
                variant_assistant_attribute_choice_ids=[
                    (0, False, dict(attribute_id=legs.id, value_id=pedestal.id)),
                ],
            ))],
        ))

# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
<commit_msg>Add docstrings to tests.<commit_after># -*- coding: utf-8 -*-

##############################################################################
#
# Sale - Product Variant Selection Assistant
# Copyright (C) 2016 OpusVL (<http://opusvl.com/>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

from openerp.tests import common
from openerp.exceptions import ValidationError

class AssistantTestCommon(common.TransactionCase):
    """Common arrangement and helpers for testing the variant assistant.
    """
    def setUp(self):
        super(AssistantTestCommon, self).setUp()
        Partner = self.env['res.partner']
        ProductTemplate = self.env['product.template']
        ProductProduct = self.env['product.product']
        SaleOrder = self.env['sale.order']
        self.partners = {}
        self.products = {}
        self.attributes = {}
        self.attr_values = {}
        self.orders = {}

        self.partners['acme'] = Partner.create(dict(
            name='TEST ACME',
            is_company=True,
            customer=True,
            supplier=False,
        ))
        self.setup_add_attribute('legs', ['Pedestal', 'Four Straight', 'Three Straight'])
        self.setup_add_attribute('maturity', ['Mild', 'Medium', 'Mature', 'Extra Mature'])
        self.setup_add_attribute('wood', ['Mahogany', 'Teak', 'MDF', 'Balsa', 'Oak'])
        self.products['table'] = ProductTemplate.create(dict(
            name='TEST Table',
            attribute_line_ids=[
                (0, False, dict(
                    attribute_id=self.attributes['legs'].id,
                    value_ids=[
                        (6, False, [v.id for v in self.attr_values['legs'].values()]),
                    ],
                )),
                (0, False, dict(
                    attribute_id=self.attributes['wood'].id,
                    value_ids=[
                        (6, False, [self.attr_values['wood'][n].id for n in ['Mahogany', 'Teak']]),
                    ],
                )),
            ],
        ))
        self.products['cheese'] = ProductTemplate.create(dict(
            name='Cheese',
            attribute_line_ids=[
                (0, False, dict(
                    attribute_id=self.attributes['maturity'].id,
                    value_ids=[
                        (6, False, [self.attr_values['maturity'][n].id for n in ['Mature', 'Extra Mature']]),
                    ]
                ))
            ],
        ))
        self.orders['FIRST'] = SaleOrder.create(dict(
            partner_id = self.partners['acme'].id,
        ))

    def setup_add_attribute(self, name, value_names):
        """Add an attribute and its values to the database.

        'TEST ' is prepended to name.

        The attribute object is added to self.attributes[name]

        For each val_name in value_names, an attribute value is created with val_name,
        attached to self.attributes[name], and registered under self.attr_values[name][val_name]
        for use in tests.
        """
        ProductAttribute = self.env['product.attribute']
        ProductAttributeValue = self.env['product.attribute.value']
        self.attributes[name] = attr = ProductAttribute.create(dict(name='TEST '+name))
        self.attr_values[name] = values = {}
        for val in value_names:
            values[val] = ProductAttributeValue.create(dict(name=val, attribute_id=attr.id))

    def create_line(self, order_name, template_name=False):
        """Naively create and return sale order line.

        The line is created on the sale order self.orders[order_name].

        The line's name (Description) will be filled in with 'NOT NULL'.

        If template_name is provided, then variant_assistant_product_template_id
        will also be set to self.products[template_name]
        """
        order = self.orders[order_name]
        return self.env['sale.order.line'].create(dict(
            order_id=order.id,
            name='NOT NULL',
            variant_assistant_product_template_id=template_name and self.products[template_name].id,
        ))

    def _select_attribute(self, line, attr_name, value_name):
        """Select value value_name for attr_name on order line.

        Attribute and value are looked up in self.attributes and self.attr_values, respectively.
        """
        line.write({
            'variant_assistant_attribute_choice_ids': [(0, False, {
                'attribute_id': self.attributes[attr_name].id,
                'value_id': self.attr_values[attr_name][value_name].id,
            })],
        })

class TestHelperTests(AssistantTestCommon):
    """Tests helper methods defined on AssistantTestCommon.
    """
    at_install = False
    post_install = True

    def test_create_line_with_template(self):
        """create_line can produce an order line with template filled in
        """
        result = self.create_line('FIRST', template_name='cheese')

        self.assertEqual(result.order_id, self.orders['FIRST'])
        self.assertEqual(result.variant_assistant_product_template_id, self.products['cheese'])
        self.assertEqual(len(result.variant_assistant_attribute_choice_ids), 0)

    def test_create_line_without_template(self):
        """create_line can produce an order line with template omitted
        """
        result = self.create_line('FIRST')

        self.assertEqual(result.order_id, self.orders['FIRST'])
        self.assertFalse(result.variant_assistant_product_template_id)

    def test_select_attribute_from_empty(self):
        """calling _select_attribute on a new line adds an attribute choice
        """
        line = self.create_line(order_name='FIRST', template_name='table')
        self._select_attribute(line, 'legs', 'Pedestal')

        result = line.variant_assistant_attribute_choice_ids

        self.assertEqual(len(result), 1)
        self.assertEqual(result[0].attribute_id, self.attributes['legs'])
        self.assertEqual(result[0].value_id, self.attr_values['legs']['Pedestal'])

class ResolverTests(AssistantTestCommon):
    """Test the logic that resolves the choices to a unique variant.
    """
    at_install = False
    post_install = True

    def test_nothing_with_no_template_selected(self):
        """Resolve to False if no template is selected.
        """
        line = self.create_line(order_name='FIRST', template_name=False)

        result = line._assistant_resolve_variant()

        self.assertFalse(result)

    def test_nothing_with_no_values_selected(self):
        """Resolve to False if a template is selected but no values are.
        """
        line = self.create_line(order_name='FIRST', template_name='table')

        result = line._assistant_resolve_variant()

        self.assertFalse(result)

    def test_nothing_with_incomplete_values_selected(self):
        """Incomplete choice of values returns False.
        """
        line = self.create_line(order_name='FIRST', template_name='table')
        self._select_attribute(line, 'legs', 'Pedestal')

        result = line._assistant_resolve_variant()

        self.assertFalse(result)

    def test_single_variant_with_complete_set_of_values(self):
        """Selecting a value for all attributes returns a single product variant.
        """
        line = self.create_line(order_name='FIRST', template_name='table')
        self._select_attribute(line, 'legs', 'Pedestal')
        self._select_attribute(line, 'wood', 'Teak')

        result = line._assistant_resolve_variant()

        self.assertEqual(len(result), 1, 'Should select exactly one variant')
        self.assertSetEqual(frozenset(result.attribute_value_ids.mapped('name')), frozenset(['Teak', 'Pedestal']),
                            "Chosen variant has values Legs and Pedestal")

class AvailableValuesTests(AssistantTestCommon):
    """Test the available values method: _assistant_available_values()
    """
    at_install = False
    post_install = True

    def test_cheese_maturities(self):
        """cheese._assistant_available_values(maturity) -> Mature, Extra Mature
        """
        line = self.create_line('FIRST', 'cheese')

        result = line._assistant_available_values(self.attributes['maturity'])

        self.assertTrue(result)
        self.assertSetEqual(frozenset(result.mapped('name')), frozenset(['Mature', 'Extra Mature']))

    def test_cheese_legs(self):
        """cheese._assistant_available_values(legs) empty
        """
        line = self.create_line('FIRST', 'cheese')

        result = line._assistant_available_values(self.attributes['legs'])

        self.assertFalse(result, 'returned recordset should be empty')

class AvailableAttributesTests(AssistantTestCommon):
    """Test the available attributes method: _assistant_attributes()
    """
    at_install = False
    post_install = True

    def test_assistant_clear_by_default(self):
        """A new order line has no product template or attribute choices.
        """
        order = self.orders['FIRST']

        line = self.env['sale.order.line'].create(dict(
            order_id=order.id,
            name='NOT NULL',
        ))

        self.assertFalse(line.variant_assistant_product_template_id)
        self.assertFalse(line.variant_assistant_attribute_choice_ids)

    def test_cheese_attributes(self):
        """_assistant_attributes() yields correct attributes for cheese
        """
        line = self.env['sale.order.line'].create(dict(
            order_id=self.orders['FIRST'].id,
            name='NOT NULL',
            variant_assistant_product_template_id=self.products['cheese'].id,
        ))

        result = line._assistant_attributes()

        self.assertSetEqual(frozenset(result.mapped('name')), frozenset(['TEST maturity']),
                            "Maturity is the only valid attribute of Cheese")

    def test_table_attributes(self):
        """_assistant_attributes() yields correct attributes for table
        """
        line = self.env['sale.order.line'].create(dict(
            order_id=self.orders['FIRST'].id,
            name='NOT NULL',
            variant_assistant_product_template_id=self.products['table'].id,
        ))

        result = line._assistant_attributes()

        self.assertSetEqual(frozenset(result.mapped('name')), frozenset(['TEST legs', 'TEST wood']))

    def test_blank_attributes(self):
        """_assistant_attributes() yields empty set if template not set.
        """
        line = self.env['sale.order.line'].create(dict(
            order_id=self.orders['FIRST'].id,
            name='NOT NULL',
        ))

        result = line._assistant_attributes()

        self.assertEqual(result, self.env['product.attribute'])

class ValidationTests(AssistantTestCommon):
    """Test validation of variant choice assistant models.

    The validation is there to help catch bugs - onupdate methods will be dealing with this.
    """
    at_install = False
    post_install = True

    def test_cheese_with_legs_option(self):
        with self.assertRaisesRegexp(ValidationError, r"Attribute must match product template"):
            cheese = self.products['cheese']
            self.orders['FIRST'].write(dict(
                order_line=[(0, False, dict(
                    name='NOT NULL',
                    variant_assistant_product_template_id=cheese.id,
                    variant_assistant_attribute_choice_ids=[
                        (0, False, dict(attribute_id=self.attributes['legs'].id)),
                    ],
                ))],
            ))

    def test_table_with_mature_legs(self):
        """Raises ValidationError if table with mature legs is selected.
        """
        with self.assertRaisesRegexp(ValidationError, r"Value must match attribute"):
            table = self.products['table']
            legs = self.attributes['legs']
            mature = self.attr_values['maturity']['Mature']
            self.orders['FIRST'].write(dict(
                order_line=[(0, False, dict(
                    name='NOT NULL',
                    variant_assistant_product_template_id=table.id,
                    variant_assistant_attribute_choice_ids=[
                        (0, False, dict(attribute_id=legs.id,
                                        value_id=mature.id)),
                    ],
                ))],
            ))

    def test_mild_cheese(self):
        """Picking mild cheese raises ValidationError.

        This is because Mild, although a valid Maturity, is not valid for this cheese.
        """
        with self.assertRaisesRegexp(ValidationError, r"Value must match product"):
            cheese = self.products['cheese']
            maturity = self.attributes['maturity']
            mild = self.attr_values['maturity']['Mild']
            self.orders['FIRST'].write(dict(
                order_line=[(0, False, dict(
                    name='NOT NULL',
                    variant_assistant_product_template_id=cheese.id,
                    variant_assistant_attribute_choice_ids=[
                        (0, False, dict(attribute_id=maturity.id, value_id=mild.id)),
                    ],
                ))]
            ))

    def test_assistant_is_optional(self):
        """It is still OK to create an order line without filling in the assistant fields.
        """
        self.orders['FIRST'].write(dict(
            order_line=[(0, False, dict(product_id=self.products['table'].product_variant_ids[0].id))],
        ))

    def test_table_with_pedestal_legs(self):
        """It is OK to choose mature cheese.
        """
        table = self.products['table']
        legs = self.attributes['legs']
        pedestal = self.attr_values['legs']['Pedestal']
        self.orders['FIRST'].write(dict(
            order_line=[(0, False, dict(
                name='NOT NULL',   # This won't matter while we're doing the onupdate
                variant_assistant_product_template_id=table.id,
                variant_assistant_attribute_choice_ids=[
                    (0, False, dict(attribute_id=legs.id, value_id=pedestal.id)),
                ],
            ))],
        ))

# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
<|endoftext|>


<commit_before>#!/usr/bin/python
# -*- coding: utf-8 -*-
###############################################################################
#
# ODOO (ex OpenERP)
# Open Source Management Solution
# Copyright (C) 2001-2015 Micronaet S.r.l. (<https://micronaet.com>)
# Developer: Nicola Riolini @thebrush (<https://it.linkedin.com/in/thebrush>)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

import os
import pdb
import sys
import logging
import openerp
import openerp.addons.decimal_precision as dp
from openerp.osv import fields, osv, expression, orm
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.tools.translate import _
from openerp.tools import (DEFAULT_SERVER_DATE_FORMAT,
    DEFAULT_SERVER_DATETIME_FORMAT,
    DATETIME_FORMATS_MAP,
    float_compare)

_logger = logging.getLogger(__name__)

class ProductBomReportLimitWizard(orm.TransientModel):
    """ Wizard for open report with limited price date
    """
    _name = 'product.bom.report.limit.wizard'

    # --------------------
    # Wizard button event:
    # --------------------
    def action_print_invoice_cost_analysis(self, cr, uid, ids, context=None):
        """ Compare price with extra period
        """
        product_pool = self.pool.get('product.product')
        line_pool = self.pool.get('account.invoice.line')
        excel_pool = self.pool.get('excel.writer')

        # ---------------------------------------------------------------------
        #                          Generate data:
        # ---------------------------------------------------------------------
        if context is None:
            context = {}
        wiz_proxy = self.browse(cr, uid, ids, context=context)[0]

        # ---------------------------------------------------------------------
        # Setup domain for invoice:
        # ---------------------------------------------------------------------
        domain = []
        from_date = wiz_proxy.from_date
        to_date = wiz_proxy.to_date
        if from_date:
            domain.append(('invoice_id.date_invoice', '>=', from_date))
        if to_date:
            domain.append(('invoice_id.date_invoice', '<=', to_date))

        # ---------------------------------------------------------------------
        # Load DB template
        # ---------------------------------------------------------------------
        product_ids = product_pool.search(cr, uid, [
            ('bom_selection', '=', True),
        ], context=context)
        bom_data = {}
        for product in product_pool.browse(
                cr, uid, product_ids, context=context):
            code5 = (product.default_code or '')[:5]
            if not code5:
                _logger.error('Prodotto senza codice: %s' % product.name)
                continue
            bom_data[code5] = product

        # ---------------------------------------------------------------------
        #                          Excel export:
        # ---------------------------------------------------------------------
        ws_name = 'Distinte'

        header = [
            'Stagione', 'Cliente', 'Fattura', 'Data',
            'Prodotto', 'Nome', 'DB',
            'Quant.', 'Pr. unit', 'Pr. Netto', 'Costo DB', 'Marg. unit.',
            '% trasp', '% extra sc.',
            'Fatt. tot.', 'Marg. tot', 'Marg. %',
            'No DB', 'Errore',
        ]
        width = [
            8, 35, 12, 10,
            15, 30, 8,
            10, 10, 10, 10, 10,
            6, 6,
            12, 12, 10,
            5, 40,
        ]

        # ---------------------------------------------------------------------
        # Create WS:
        # ---------------------------------------------------------------------
        excel_pool.create_worksheet(name=ws_name)
        excel_pool.column_width(ws_name, width)
        # excel_pool.row_height(ws_name, row_list, height=10)
        title = ['Controllo margini fatturato']

        # ---------------------------------------------------------------------
        # Generate format used:
        # ---------------------------------------------------------------------
        excel_pool.set_format(number_format='#,##0.#0')
        excel_format = {
            'title': excel_pool.get_format(key='title'),
            'header': excel_pool.get_format(key='header'),
            'white': {
                'text': excel_pool.get_format(key='text'),
                'number': excel_pool.get_format(key='number'),
            },
            'yellow': {
                'text': excel_pool.get_format(key='bg_yellow'),
                'number': excel_pool.get_format(key='bg_yellow_number'),
            },
            'red': {
                'text': excel_pool.get_format(key='bg_red'),
                'number': excel_pool.get_format(key='bg_red_number'),
            },
            'green': {
                'text': excel_pool.get_format(key='bg_green'),
                'number': excel_pool.get_format(key='bg_green_number'),
            },
        }

        # ---------------------------------------------------------------------
        # Write title / header
        # ---------------------------------------------------------------------
        row = 0
        # Total line:
        excel_pool.write_xls_line(
            ws_name, row, [
                u'Analisi marginalità su fatturato:'
            ], default_format=excel_format['title'])
        excel_pool.merge_cell(ws_name, [row, 0, row, 11])

        row += 1
        excel_pool.write_xls_line(
            ws_name, row, header, default_format=excel_format['header'])
        excel_pool.freeze_panes(ws_name, 2, 5)
        excel_pool.autofilter(ws_name, row, 0, row, len(header) - 1)

        partner_cache = {}
        line_ids = line_pool.search(cr, uid, domain, context=context)
        for line in sorted(
                line_pool.browse(cr, uid, line_ids, context=context),
                key=lambda l: (
                    l.invoice_id.partner_id.name,
                    l.invoice_id.date_invoice,
                )):

            # -----------------------------------------------------------------
            # Read data:
            # -----------------------------------------------------------------
            invoice = line.invoice_id
            partner = invoice.partner_id
            product = line.product_id
            code5 = (product.default_code or '')[:5]
            quantity = line.quantity

            # Cache parameter
            if partner not in partner_cache:
                partner_cache[partner] = [
                    partner.industrial_transport_rate,
                    partner.industrial_extra_discount,
                ]
            industrial_transport_rate, industrial_extra_discount = \
                partner_cache[partner]

            # -----------------------------------------------------------------
            # Calc data used:
            # -----------------------------------------------------------------
            subtotal = line.price_subtotal
            if quantity:
                real_price = subtotal / quantity
                # A. Extra discount:
                real_price -= real_price * industrial_extra_discount / 100.0
            else:
                real_price = 0.0

            # Bom price
            bom_product = bom_data.get(code5)
            margin_rate = 0.0
            if bom_product:
                cost = bom_product.to_industrial

                # B. Extra transport cost:
                if industrial_transport_rate:
                    cost += cost * industrial_transport_rate / 100.0

                margin = real_price - cost
                margin_total = margin * quantity
                if subtotal:
                    margin_rate = 100.0 * margin_total / subtotal
                db = code5
                error = u''
            else:
                cost = margin = margin_total = 0.0  # no value if no cost!
                db = u''
                error = u'DB non trovata (niente margine)'

            # -----------------------------------------------------------------
            # Color setup:
            # -----------------------------------------------------------------
            if real_price < 0:
                color = excel_format['red']
            elif not margin:
                color = excel_format['yellow']
            else:
                color = excel_format['white']

            # -----------------------------------------------------------------
            # Write data:
            # -----------------------------------------------------------------
            data = [
                line.season_period,
                u'{}'.format(partner.name),
                invoice.number,
                invoice.date_invoice,

                u'{}'.format(product.default_code),
                u'{}'.format(product.name),
                db,

                quantity,
                (line.price_unit, color['number']),
                (real_price, color['number']),
                (cost, color['number']),
                (margin, color['number']),

                (industrial_transport_rate, color['number']),
                (industrial_extra_discount, color['number']),

                (subtotal, color['number']),
                (margin_total, color['number']),
                (margin_rate, color['number']),

                u'' if db else u'X',
                error,
            ]
            row += 1
            excel_pool.write_xls_line(
                ws_name, row, data, default_format=color['text'])

        # ---------------------------------------------------------------------
        # Update with total:
        # ---------------------------------------------------------------------
        total_row = row - 1
        row = 0
        # todo keep updated if change columns:
        for col in (14, 15):
            from_cell = excel_pool.rowcol_to_cell(row + 2, col)
            to_cell = excel_pool.rowcol_to_cell(1 + row + total_row, col)
            excel_pool.write_formula(
                ws_name,
                row, col, u"=SUBTOTAL(9,%s:%s)" % (from_cell, to_cell),
                excel_format['green']['number'],
                0.0,  # complete_total[position],
            )
        excel_pool.write_formula(
            ws_name,
            row, 16, u'= 100 * P1 / O1',
            excel_format['green']['number'],
            0.0,  # complete_total[position],
        )

        return excel_pool.return_attachment(cr, uid, 'Comparativo fatturato')

    def action_print_extra_period(self, cr, uid, ids, context=None):
        """ Compare price with extra period
        """
        product_pool = self.pool.get('product.product')
        excel_pool = self.pool.get('excel.writer')

        # ---------------------------------------------------------------------
        #                          Generate data:
        # ---------------------------------------------------------------------
        if context is None:
            context = {}

        wiz_proxy = self.browse(cr, uid, ids, context=context)[0]
        master_data = {}
        from_date = '1975-01-01'
        references = ['']
        for reference in wiz_proxy.extra_period.split(';'):
            reference = reference.strip()
            to_date = '%s-%s-%s' % (
                reference[-4:],
                reference[3:5],
                reference[:2],
            )
            references.append(to_date)
        references.sort()
        _logger.warning('Multi report for date: %s' % (references, ))

        # First reference normal report!
        for reference in references:
            if reference:
                datas = {
                    'wizard': True,
                    'from_date': from_date,
                    'to_date': reference,
                }
            else:
                datas = {
                    'wizard': True,
                }
            records = product_pool.report_get_objects_bom_industrial_cost(
                    cr, uid, datas=datas, context=context)

            for record in records:
                # Explode record:
                (min_price, max_price, error, components, extra1, extra2,
                 index, total, product, parameter, total_text,
                 pipe_total_weight, simulated_price) = record

                # Write all price present (False = reference for 100%)
                # todo manage error field?
                if product not in master_data:
                        master_data[product] = ({}, simulated_price)
                master_data[product][0][reference] = max_price  # Save this

        # ---------------------------------------------------------------------
        #                          Excel export:
        # ---------------------------------------------------------------------
        ws_name = 'Distinte'
        header = [
            'Codice',
            'Descrizione',
            'Prezzo riferimento',

            'Prezzo simulato',
            'Variaz. %',
        ]
        width = [
            20, 40, 15,
            15, 10,
        ]
        for reference in references[1:]:
            header.append('Al %s' % reference)
            header.append('Diff. %')
            width.append(15)
            width.append(10)

        # ---------------------------------------------------------------------
        # Create WS:
        # ---------------------------------------------------------------------
        excel_pool.create_worksheet(name=ws_name)
        excel_pool.column_width(ws_name, width)
        # excel_pool.row_height(ws_name, row_list, height=10)
        title = ('Confronto distinte base',)

        # ---------------------------------------------------------------------
        # Generate format used:
        # ---------------------------------------------------------------------
        excel_pool.set_format()
        f_title = excel_pool.get_format(key='title')
        f_header = excel_pool.get_format(key='header')
        f_text = excel_pool.get_format(key='text')
        f_number = excel_pool.get_format(key='number')

        # ---------------------------------------------------------------------
        # Write title / header
        # ---------------------------------------------------------------------
        row = 0
        excel_pool.write_xls_line(
            ws_name, row, [title], default_format=f_title)

        row += 1
        excel_pool.write_xls_line(
            ws_name, row, header, default_format=f_header)

        # ---------------------------------------------------------------------
        # Product selection:
        # ---------------------------------------------------------------------
        for product in sorted(master_data, key=lambda x: x.default_code):
            row += 1
            prices, simulated_price = master_data[product]
            default_code = product.default_code or ''
            current_price = prices['']
            if current_price:
                simulated_rate = \
                    (simulated_price - current_price) / current_price * 100.0
            else:
                simulated_rate = '/'
            line = [
                default_code or '',
                u'%s' % product.name,
                (current_price, f_number),
                (simulated_price, f_number),
                (simulated_rate, f_number),
            ]

            # Append other record data
            for reference in references[1:]:
                this_price = prices.get(reference, 0.0)
                line.append(this_price)
                if current_price:
                    this_rate = \
                        (this_price - current_price) / current_price * 100.0
                else:
                    this_rate = 0.0
                # Calc rate difference
                line.append(this_rate)
            excel_pool.write_xls_line(
                ws_name, row, line, default_format=f_text)
        return excel_pool.return_attachment(cr, uid, 'Distinte base')

    def action_print(self, cr, uid, ids, context=None):
        """ Event for print report
        """
        if context is None:
            context = {}

        wiz_proxy = self.browse(cr, uid, ids, context=context)[0]
        from_date = wiz_proxy.from_date or False
        to_date = wiz_proxy.to_date or False
        report_name = wiz_proxy.report_name
        if not to_date or not from_date:
            raise osv.except_osv(
                _('Errore report'),
                _('Il range di date è obbligatorio'),
                )
        datas = {
            'wizard': True,
            'from_date': from_date,
            'to_date': to_date,
            }

        return {
            'type': 'ir.actions.report.xml',
            'report_name': report_name,
            'datas': datas,
            }

    _columns = {
        'from_date': fields.date('From date'),
        'to_date': fields.date('To date'),
        'report_name': fields.selection([
            ('industrial_cost_bom_report', 'Con costi'),
            ('industrial_cost_bom_no_price_report', 'Senza costi'),
        ], 'Tipo report', required=True),
        'extra_period': fields.char(
            'Date valutazione',
            help='Periodi extra l\'attuale, es. per avere gli ultrimi 3 anni:'
                 '01/09/2020;01/09/2019;01/09/2018'),
        }

    _defaults = {
        'report_name': lambda *x: 'industrial_cost_bom_report',
    }
<commit_msg>todo add incoterms<commit_after>#!/usr/bin/python
# -*- coding: utf-8 -*-
###############################################################################
#
# ODOO (ex OpenERP)
# Open Source Management Solution
# Copyright (C) 2001-2015 Micronaet S.r.l. (<https://micronaet.com>)
# Developer: Nicola Riolini @thebrush (<https://it.linkedin.com/in/thebrush>)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

import os
import pdb
import sys
import logging
import openerp
import openerp.addons.decimal_precision as dp
from openerp.osv import fields, osv, expression, orm
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.tools.translate import _
from openerp.tools import (DEFAULT_SERVER_DATE_FORMAT,
    DEFAULT_SERVER_DATETIME_FORMAT,
    DATETIME_FORMATS_MAP,
    float_compare)

_logger = logging.getLogger(__name__)

class ProductBomReportLimitWizard(orm.TransientModel):
    """ Wizard for open report with limited price date
    """
    _name = 'product.bom.report.limit.wizard'

    # --------------------
    # Wizard button event:
    # --------------------
    def action_print_invoice_cost_analysis(self, cr, uid, ids, context=None):
        """ Compare price with extra period
        """
        product_pool = self.pool.get('product.product')
        line_pool = self.pool.get('account.invoice.line')
        excel_pool = self.pool.get('excel.writer')

        # ---------------------------------------------------------------------
        #                          Generate data:
        # ---------------------------------------------------------------------
        if context is None:
            context = {}
        wiz_proxy = self.browse(cr, uid, ids, context=context)[0]

        # ---------------------------------------------------------------------
        # Setup domain for invoice:
        # ---------------------------------------------------------------------
        domain = []
        from_date = wiz_proxy.from_date
        to_date = wiz_proxy.to_date
        if from_date:
            domain.append(('invoice_id.date_invoice', '>=', from_date))
        if to_date:
            domain.append(('invoice_id.date_invoice', '<=', to_date))

        # ---------------------------------------------------------------------
        # Load DB template
        # ---------------------------------------------------------------------
        product_ids = product_pool.search(cr, uid, [
            ('bom_selection', '=', True),
        ], context=context)
        bom_data = {}
        for product in product_pool.browse(
                cr, uid, product_ids, context=context):
            code5 = (product.default_code or '')[:5]
            if not code5:
                _logger.error('Prodotto senza codice: %s' % product.name)
                continue
            bom_data[code5] = product

        # ---------------------------------------------------------------------
        #                          Excel export:
        # ---------------------------------------------------------------------
        ws_name = 'Distinte'

        header = [
            'Stagione', 'Cliente', 'Fattura', 'Data',
            'Prodotto', 'Nome', 'DB',
            'Quant.', 'Pr. unit', 'Pr. Netto', 'Costo DB', 'Marg. unit.',
            '% trasp', '% extra sc.',
            'Fatt. tot.', 'Marg. tot', 'Marg. %',
            'No DB', 'Errore',
        ]
        width = [
            8, 35, 12, 10,
            15, 30, 8,
            10, 10, 10, 10, 10,
            6, 6,
            12, 12, 10,
            5, 40,
        ]

        # ---------------------------------------------------------------------
        # Create WS:
        # ---------------------------------------------------------------------
        excel_pool.create_worksheet(name=ws_name)
        excel_pool.column_width(ws_name, width)
        # excel_pool.row_height(ws_name, row_list, height=10)
        title = ['Controllo margini fatturato']

        # ---------------------------------------------------------------------
        # Generate format used:
        # ---------------------------------------------------------------------
        excel_pool.set_format(number_format='#,##0.#0')
        excel_format = {
            'title': excel_pool.get_format(key='title'),
            'header': excel_pool.get_format(key='header'),
            'white': {
                'text': excel_pool.get_format(key='text'),
                'number': excel_pool.get_format(key='number'),
            },
            'yellow': {
                'text': excel_pool.get_format(key='bg_yellow'),
                'number': excel_pool.get_format(key='bg_yellow_number'),
            },
            'red': {
                'text': excel_pool.get_format(key='bg_red'),
                'number': excel_pool.get_format(key='bg_red_number'),
            },
            'green': {
                'text': excel_pool.get_format(key='bg_green'),
                'number': excel_pool.get_format(key='bg_green_number'),
            },
        }

        # ---------------------------------------------------------------------
        # Write title / header
        # ---------------------------------------------------------------------
        row = 0
        # Total line:
        excel_pool.write_xls_line(
            ws_name, row, [
                u'Analisi marginalità su fatturato:'
            ], default_format=excel_format['title'])
        excel_pool.merge_cell(ws_name, [row, 0, row, 11])

        row += 1
        excel_pool.write_xls_line(
            ws_name, row, header, default_format=excel_format['header'])
        excel_pool.freeze_panes(ws_name, 2, 5)
        excel_pool.autofilter(ws_name, row, 0, row, len(header) - 1)

        partner_cache = {}
        line_ids = line_pool.search(cr, uid, domain, context=context)
        for line in sorted(
                line_pool.browse(cr, uid, line_ids, context=context),
                key=lambda l: (
                    l.invoice_id.partner_id.name,
                    l.invoice_id.date_invoice,
                )):

            # -----------------------------------------------------------------
            # Read data:
            # -----------------------------------------------------------------
            invoice = line.invoice_id
            partner = invoice.partner_id
            product = line.product_id
            code5 = (product.default_code or '')[:5]
            quantity = line.quantity
            # order = line.order_id  todo get incoterms here to test

            # Cache parameter
            if partner not in partner_cache:
                partner_cache[partner] = [
                    partner.industrial_transport_rate,
                    partner.industrial_extra_discount,
                ]
            industrial_transport_rate, industrial_extra_discount = \
                partner_cache[partner]

            # -----------------------------------------------------------------
            # Calc data used:
            # -----------------------------------------------------------------
            subtotal = line.price_subtotal
            if quantity:
                real_price = subtotal / quantity
                # A. Extra discount:
                real_price -= real_price * industrial_extra_discount / 100.0
            else:
                real_price = 0.0

            # Bom price
            bom_product = bom_data.get(code5)
            margin_rate = 0.0
            if bom_product:
                cost = bom_product.to_industrial

                # B. Extra transport cost:
                # todo manage only if incoterms:
                if industrial_transport_rate:
                    cost += cost * industrial_transport_rate / 100.0

                margin = real_price - cost
                margin_total = margin * quantity
                if subtotal:
                    margin_rate = 100.0 * margin_total / subtotal
                db = code5
                error = u''
            else:
                cost = margin = margin_total = 0.0  # no value if no cost!
                db = u''
                error = u'DB non trovata (niente margine)'

            # -----------------------------------------------------------------
            # Color setup:
            # -----------------------------------------------------------------
            if real_price < 0:
                color = excel_format['red']
            elif not margin:
                color = excel_format['yellow']
            else:
                color = excel_format['white']

            # -----------------------------------------------------------------
            # Write data:
            # -----------------------------------------------------------------
            data = [
                line.season_period,
                u'{}'.format(partner.name),
                invoice.number,
                invoice.date_invoice,

                u'{}'.format(product.default_code),
                u'{}'.format(product.name),
                db,

                quantity,
                (line.price_unit, color['number']),
                (real_price, color['number']),
                (cost, color['number']),
                (margin, color['number']),

                (industrial_transport_rate, color['number']),
                (industrial_extra_discount, color['number']),

                (subtotal, color['number']),
                (margin_total, color['number']),
                (margin_rate, color['number']),

                u'' if db else u'X',
                error,
            ]
            row += 1
            excel_pool.write_xls_line(
                ws_name, row, data, default_format=color['text'])

        # ---------------------------------------------------------------------
        # Update with total:
        # ---------------------------------------------------------------------
        total_row = row - 1
        row = 0
        # todo keep updated if change columns:
        for col in (14, 15):
            from_cell = excel_pool.rowcol_to_cell(row + 2, col)
            to_cell = excel_pool.rowcol_to_cell(1 + row + total_row, col)
            excel_pool.write_formula(
                ws_name,
                row, col, u"=SUBTOTAL(9,%s:%s)" % (from_cell, to_cell),
                excel_format['green']['number'],
                0.0,  # complete_total[position],
            )
        excel_pool.write_formula(
            ws_name,
            row, 16, u'= 100 * P1 / O1',
            excel_format['green']['number'],
            0.0,  # complete_total[position],
        )

        return excel_pool.return_attachment(cr, uid, 'Comparativo fatturato')

    def action_print_extra_period(self, cr, uid, ids, context=None):
        """ Compare price with extra period
        """
        product_pool = self.pool.get('product.product')
        excel_pool = self.pool.get('excel.writer')

        # ---------------------------------------------------------------------
        #                          Generate data:
        # ---------------------------------------------------------------------
        if context is None:
            context = {}

        wiz_proxy = self.browse(cr, uid, ids, context=context)[0]
        master_data = {}
        from_date = '1975-01-01'
        references = ['']
        for reference in wiz_proxy.extra_period.split(';'):
            reference = reference.strip()
            to_date = '%s-%s-%s' % (
                reference[-4:],
                reference[3:5],
                reference[:2],
            )
            references.append(to_date)
        references.sort()
        _logger.warning('Multi report for date: %s' % (references, ))

        # First reference normal report!
        for reference in references:
            if reference:
                datas = {
                    'wizard': True,
                    'from_date': from_date,
                    'to_date': reference,
                }
            else:
                datas = {
                    'wizard': True,
                }
            records = product_pool.report_get_objects_bom_industrial_cost(
                    cr, uid, datas=datas, context=context)

            for record in records:
                # Explode record:
                (min_price, max_price, error, components, extra1, extra2,
                 index, total, product, parameter, total_text,
                 pipe_total_weight, simulated_price) = record

                # Write all price present (False = reference for 100%)
                # todo manage error field?
                if product not in master_data:
                        master_data[product] = ({}, simulated_price)
                master_data[product][0][reference] = max_price  # Save this

        # ---------------------------------------------------------------------
        #                          Excel export:
        # ---------------------------------------------------------------------
        ws_name = 'Distinte'
        header = [
            'Codice',
            'Descrizione',
            'Prezzo riferimento',

            'Prezzo simulato',
            'Variaz. %',
        ]
        width = [
            20, 40, 15,
            15, 10,
        ]
        for reference in references[1:]:
            header.append('Al %s' % reference)
            header.append('Diff. %')
            width.append(15)
            width.append(10)

        # ---------------------------------------------------------------------
        # Create WS:
        # ---------------------------------------------------------------------
        excel_pool.create_worksheet(name=ws_name)
        excel_pool.column_width(ws_name, width)
        # excel_pool.row_height(ws_name, row_list, height=10)
        title = ('Confronto distinte base',)

        # ---------------------------------------------------------------------
        # Generate format used:
        # ---------------------------------------------------------------------
        excel_pool.set_format()
        f_title = excel_pool.get_format(key='title')
        f_header = excel_pool.get_format(key='header')
        f_text = excel_pool.get_format(key='text')
        f_number = excel_pool.get_format(key='number')

        # ---------------------------------------------------------------------
        # Write title / header
        # ---------------------------------------------------------------------
        row = 0
        excel_pool.write_xls_line(
            ws_name, row, [title], default_format=f_title)

        row += 1
        excel_pool.write_xls_line(
            ws_name, row, header, default_format=f_header)

        # ---------------------------------------------------------------------
        # Product selection:
        # ---------------------------------------------------------------------
        for product in sorted(master_data, key=lambda x: x.default_code):
            row += 1
            prices, simulated_price = master_data[product]
            default_code = product.default_code or ''
            current_price = prices['']
            if current_price:
                simulated_rate = \
                    (simulated_price - current_price) / current_price * 100.0
            else:
                simulated_rate = '/'
            line = [
                default_code or '',
                u'%s' % product.name,
                (current_price, f_number),
                (simulated_price, f_number),
                (simulated_rate, f_number),
            ]

            # Append other record data
            for reference in references[1:]:
                this_price = prices.get(reference, 0.0)
                line.append(this_price)
                if current_price:
                    this_rate = \
                        (this_price - current_price) / current_price * 100.0
                else:
                    this_rate = 0.0
                # Calc rate difference
                line.append(this_rate)
            excel_pool.write_xls_line(
                ws_name, row, line, default_format=f_text)
        return excel_pool.return_attachment(cr, uid, 'Distinte base')

    def action_print(self, cr, uid, ids, context=None):
        """ Event for print report
        """
        if context is None:
            context = {}

        wiz_proxy = self.browse(cr, uid, ids, context=context)[0]
        from_date = wiz_proxy.from_date or False
        to_date = wiz_proxy.to_date or False
        report_name = wiz_proxy.report_name
        if not to_date or not from_date:
            raise osv.except_osv(
                _('Errore report'),
                _('Il range di date è obbligatorio'),
                )
        datas = {
            'wizard': True,
            'from_date': from_date,
            'to_date': to_date,
            }

        return {
            'type': 'ir.actions.report.xml',
            'report_name': report_name,
            'datas': datas,
            }

    _columns = {
        'from_date': fields.date('From date'),
        'to_date': fields.date('To date'),
        'report_name': fields.selection([
            ('industrial_cost_bom_report', 'Con costi'),
            ('industrial_cost_bom_no_price_report', 'Senza costi'),
        ], 'Tipo report', required=True),
        'extra_period': fields.char(
            'Date valutazione',
            help='Periodi extra l\'attuale, es. per avere gli ultrimi 3 anni:'
                 '01/09/2020;01/09/2019;01/09/2018'),
        }

    _defaults = {
        'report_name': lambda *x: 'industrial_cost_bom_report',
    }
<|endoftext|>


<commit_before>"""

    wind.web.stream
    ~~~~~~~~~~~~~~~

    Provides models for handling socket io stream.

"""

import errno
import socket
from wind.looper import Looper
from wind.poll import PollEvents
from wind.exceptions import StreamError
from wind.web.datastructures import FlexibleDeque
from wind.socketserver import EWOULDBLOCK, ECONNRESET

class StreamBuffer(FlexibleDeque):
    """Buffer for stream read and write."""

    def __init__(self, *args, **kwargs):
        """Initialize `_frozen` to False.
        `_frozen` is flag used to check whether current buffer
        is available for reading from or writing to.

        """
        self._frozen = False

    @property
    def frozen(self):
        return self._frozen

    @frozen.setter
    def frozen(self, value):
        self._frozen = value

class BaseStream(object):
    """Base class for io stream classes.
    Provide methods to read from and write to file or socket.
    This class can handle read, write methods asynchronously
    by attaching callback when calling method.

    Methods for the caller:

    - __init__(chunk_size=4096)
    - open()
    - close()
    - read_bytes(num_bytes)
    - read_until(delimiter)
    - write(chunk)

    Methods should be overrided

    - _read_from_fd()
    - _write_to_fd(chunk)

    """

    def __init__(self, looper=None, chunk_size=4096):
        """Initialize and open base stream.

        @param chunk_size : chunk size for read.

        """
        self._looper = looper or Looper.instance()
        self._read_buffer = StreamBuffer()
        self._write_buffer = StreamBuffer()
        self._read_chunk_size = chunk_size
        self._write_chunk_size = 128 * 1024
        self._read_buffer_bytes = 0
        self._is_opened = False

        # Stream should save this flags because read, write should be started
        # with last states when read, write was excuted by event handler.
        self._bytes_to_read = None
        self._delimiter = None
        self._include_delimiter = None

        # Saves asynchronous event handled by `looper`. (PollEvents)
        self._handler_event = None

        self._read_callback = None
        self._write_callback = None
        self._close_callback = None

        self.open()

    def open(self):
        self._is_opened = True

    def close(self):
        if not self.closed:
            self._clear()
            self._is_opened = False
            self._close_fd()
            self._run_close_callback()

    def _clear(self):
        """Make this stream to initial state after served one request"""
        if self._handler_event is not None:
            slef._handler_event = None
            self._looper.remove_handler(self.fileno())
        self._read_callback = self._write_callback = None
        self._read_buffer_bytes = 0
        self._read_buffer =  self._write_buffer = None

    def _close_fd(self):
        raise NotImplementedError

    def fileno(self):
        """Returns fd of socket or file"""
        raise NotImplementedError

    @property
    def closed(self):
        return not self._is_opened

    @property
    def reading(self):
        return self._read_callback is not None

    @property
    def writing(self):
        return self._write_callback is not None

    def read_bytes(self, bytes_to_read, callback):
        """Read `bytes_to_read` bytes from file"""
        if not isinstance(bytes_to_read, int):
            raise StreamError('`read_bytes` can only accept `int` param')
        self._bytes_to_read = bytes_to_read

        self._add_callback(callback)
        self._process_read()

    def read_until(self, delimiter, callback, include=False):
        """Read until first occurrence of `delimiter`.
        Returned chunk that contains `delimiter`

        @param include(optional): if True, include `delimiter` in chunk.

        """
        if not isinstance(delimiter, basestring):
            raise StreamError('`read_until` can only accept `str` param')
        self._delimiter = delimiter
        self._include_delimiter = include

        self._add_callback(callback)
        self._process_read()

    def _process_read(self):
        """fd -> read buffer -> memory"""
        while not self.closed:
            if self._to_read_buffer() == 0:
                # End of read
                break
        if not self._read_buffer:
            self.close()
            return

        self._read()

    def _to_read_buffer(self):
        """Read chunk from socket or file and returns number of bytes read.

        """
        try:
            chunk = self._read_from_fd()
            if not chunk or chunk is None:
                return 0
        except socket.error as e:
            self.close()
            return

        # No buffer size limit yet.
        self._read_buffer.append(chunk)
        self._read_buffer_bytes += len(chunk)
        return len(chunk)

    def _read(self):
        """Read chunk from `_read_buffer` and run callback with that chunk.

        """
        self._raise_if_closed()

        # XXX: handle all expectable cases
        read_bytes = 0
        if self._bytes_to_read is not None:
            read_bytes = min(self._bytes_to_read, self._read_buffer_bytes)
            self._run_callback(
                self._pop_callback(), self._pop_chunk(read_bytes))

        if self._delimiter is not None:
            while True:
                if not self._read_buffer:
                    break

                pos = self._read_buffer[0].find(self._delimiter)
                if pos != -1:
                    # Found delimiter
                    if self._include_delimiter:
                        pos += len(self._delimiter)
                    self._delimiter = None
                    self._run_callback(
                        self._pop_callback(), self._pop_chunk(pos))
                    break

                if len(self._read_buffer) == 1:
                    # No delimiter found in whole read buffer.
                    break

                # No delimiter found in first chunk.
                self._read_buffer.gather(
                    len(self._read_buffer[0] + self._read_buffer[1]))

    def _pop_chunk(self, read_bytes):
        """Pop chunk from `_read_buffer` and Returns chunk."""
        self._read_buffer_bytes -= read_bytes
        self._read_buffer.gather(read_bytes)
        return self._read_buffer.popleft()

    def _read_from_fd(self):
        raise NotImplementedError()

    def write(self, chunk, callback):
        if not isinstance(chunk, basestring):
            raise StreamError('Can write only chunk of `bytes`')

        self._add_callback(callback, read=False)
        self._process_write(chunk)

    def _process_write(self, chunk, partial=False):
        self._raise_if_closed()

        if not partial:
            self._to_write_buffer(chunk)

        written = None
        while self._write_buffer:
            try:
                num_bytes = self._write_to_fd(self._write_buffer[0])
                if num_bytes == 0:
                    self._write_buffer.frozen = True
                    break
                self._write_buffer.frozen = False

                # Partial writing is handled here.
                self._write_buffer.gather(num_bytes)
                written = self._write_buffer.popleft()
            except socket.error as e:
                if e.args[0] in EWOULDBLOCK:
                    # Freeze
                    self._write_buffer.frozen = True
                elif e.args[0] in ECONNRESET:
                    self.close()
                else:
                    raise StreamError(e)
                break

        # Post write process.
        if self._write_buffer and not partial:
            # Writing is not completed at one go
            self._attach_stream_handler(PollEvents.WRITE)
        elif not self._write_buffer:
            self._run_callback(self._pop_callback(read=False), written)

    def _to_write_buffer(self, chunk):
        """Fill `_write_buffer` after dividing `chunk` with
        `_write_chunk_size` bytes blocks.

        """

        if chunk:
            for i in range(0, len(chunk), self._write_chunk_size):
                self._write_buffer.append(chunk[0:i + self._write_chunk_size])

    def _write_to_fd(self, chunk):
        raise NotImplementedError()

    def _raise_if_closed(self):
        if self.closed:
            raise StreamError('Cannot read because stream is already closed')

    def _pop_callback(self, read=True):
        """Returns saved callback and clear read or write callback

        @param read (optional): True if read callback else write callback.
        """
        if read:
            callback = self._read_callback
            self._read_callback = None
        else:
            callback = self._write_callback
            self._write_callback = None
        return callback

    def _add_callback(self, callback, read=True):
        """Check whether passed callback has acceptable form.
        We imported `inspect` here because it's not on the mainstream.

        @param callback: Callback method to be saved.
        @param read (optional): True if read callback else write callback.
        """
        from inspect import getargspec
        if callback is None:
            return

        if not hasattr(callback, '__call__'):
            raise StreamError('Stream callback is not callable')

        spec = getargspec(callback)
        if not spec.args:
            raise StreamError(
                'Should provide room for chunk in stream callback')

        if read:
            self._read_callback = callback
        else:
            self._write_callback = callback

    def _run_callback(self, callback, *args):
        """Immediately run saved callback"""
        try:
            if callback is not None:
                callback(*args)
        except Exception as e:
            self.close()
            # We don't wrap exception here because we don't know
            # what caused exception at all.
            raise

    def _run_close_callback(self):
        if self.closed:
            if self._close_callback is not None:
                callback = self._close_callback
                self._run_callback(callback)
                self._close_callback = None

    def event_handler(self, fd, events):
        """Handler which will attached to `looper`"""
        try:
            if events & PollEvents.READ:
                self._handle_read()

            if events & PollEvents.WRITE:
                self._handle_write()

            if events & PollEvents.ERROR:
                # TODO: Should handle PollEvents.ERROR
                pass

            if self.closed:
                return

        except Exception as e:
            raise StreamError(e)

    def _handle_write(self):
        """Handle write process when fd is available.
        This method will be passed to event handler of `looper`

        """
        try:
            self._process_write()
        except Exception:
            self.close()

    def _handle_read(self):
        """Handle read process when fd is available.
        This method will be passed to event handler of `looper`

        """
        try:
            self._process_read()
        except Exception:
            self.close()

    def _attach_stream_handler(self, event_mask):
        """Attach handler to `looper` for the purpose of handling
        asynchronous reading and writing"""
        if self._handler_event is None:
            # Attach new handler
            self._handler_event = event_mask | PollEvents.ERROR
            self._looper.attach_handler(
                self.fileno(), self._handler_event, self.event_handler)
        elif not self._handler_event & event_mask:
            # Update event of existing handler
            self._handler_event |= event_mask
            self._looper.update_handler(self.fileno(), event_mask)

class SocketStream(BaseStream):
    def __init__(self, socket_, *args, **kwargs):
        if not isinstance(socket_, socket.socket):
            raise StreamError(
                'SocketStream can only be initialized with `socket.socket`')
        self.socket = socket_
        super(SocketStream, self).__init__(*args, **kwargs)

    def fileno(self):
        return self.socket.fileno()

    def _read_from_fd(self):
        try:
            chunk = self.socket.recv(self._read_chunk_size)
            if not chunk:
                # Should close stream here because nothing is left to be read.
                self.close()
                return None
            return chunk
        except socket.error as e:
            if e.args[0] not in EWOULDBLOCK:
                raise
            return None

    def _write_to_fd(self, chunk):
        return self.socket.send(chunk)

    def _close_fd(self):
        self.socket.close()
        self.socket = None

class FileStream(BaseStream):
    def __init__(self, file_, *args, **kwargs):
        if not isinstance(file_, file):
            raise StreamError(
                'FileStream can only be initialized with `file`')
        self.file_ = file_
        super(FileStream, self).__init__(*args, **kwargs)

    def fileno(self):
        return self.file_.fileno()

    def _read_from_fd(self):
        return self.file_.read(self._read_chunk_size)

    def _write_to_fd(self, chunk):
        try:
            self.file_.write(chunk)
        except IOError as e:
            raise StreamError(e)

    def _close_fd(self):
        self.file_.close()
        self.file_ = None

<commit_msg>added _attach_read_handler<commit_after>"""

    wind.web.stream
    ~~~~~~~~~~~~~~~

    Provides models for handling socket io stream.

"""

import errno
import socket
from wind.looper import Looper
from wind.poll import PollEvents
from wind.exceptions import StreamError
from wind.web.datastructures import FlexibleDeque
from wind.socketserver import EWOULDBLOCK, ECONNRESET

class StreamBuffer(FlexibleDeque):
    """Buffer for stream read and write."""

    def __init__(self, *args, **kwargs):
        """Initialize `_frozen` to False.
        `_frozen` is flag used to check whether current buffer
        is available for reading from or writing to.

        """
        self._frozen = False

    @property
    def frozen(self):
        return self._frozen

    @frozen.setter
    def frozen(self, value):
        self._frozen = value

class BaseStream(object):
    """Base class for io stream classes.
    Provide methods to read from and write to file or socket.
    This class can handle read, write methods asynchronously
    by attaching callback when calling method.

    Methods for the caller:

    - __init__(chunk_size=4096)
    - open()
    - close()
    - read_bytes(num_bytes)
    - read_until(delimiter)
    - write(chunk)

    Methods should be overrided

    - _read_from_fd()
    - _write_to_fd(chunk)

    """

    def __init__(self, looper=None, chunk_size=4096):
        """Initialize and open base stream.

        @param chunk_size : chunk size for read.

        """
        self._looper = looper or Looper.instance()
        self._read_buffer = StreamBuffer()
        self._write_buffer = StreamBuffer()
        self._read_chunk_size = chunk_size
        self._write_chunk_size = 128 * 1024
        self._read_buffer_bytes = 0
        self._is_opened = False

        # Stream should save this flags because read, write should be started
        # with last states when read, write was excuted by event handler.
        self._bytes_to_read = None
        self._delimiter = None
        self._include_delimiter = None

        # Saves asynchronous event handled by `looper`. (PollEvents)
        self._handler_event = None

        self._read_callback = None
        self._write_callback = None
        self._close_callback = None

        self.open()

    def open(self):
        self._is_opened = True

    def close(self):
        if not self.closed:
            self._clear()
            self._is_opened = False
            self._close_fd()
            self._run_close_callback()

    def _clear(self):
        """Make this stream to initial state after served one request"""
        if self._handler_event is not None:
            self._handler_event = None
            self._looper.remove_handler(self.fileno())
        self._read_callback = self._write_callback = None
        self._read_buffer_bytes = 0
        self._read_buffer =  self._write_buffer = None

    def _close_fd(self):
        raise NotImplementedError

    def fileno(self):
        """Returns fd of socket or file"""
        raise NotImplementedError

    @property
    def closed(self):
        return not self._is_opened

    @property
    def reading(self):
        return self._read_callback is not None

    @property
    def writing(self):
        return self._write_callback is not None

    def read_bytes(self, bytes_to_read, callback):
        """Read `bytes_to_read` bytes from file"""
        if not isinstance(bytes_to_read, int):
            raise StreamError('`read_bytes` can only accept `int` param')
        self._bytes_to_read = bytes_to_read

        self._add_callback(callback)
        self._process_read()

    def read_until(self, delimiter, callback, include=False):
        """Read until first occurrence of `delimiter`.
        Returned chunk that contains `delimiter`

        @param include(optional): if True, include `delimiter` in chunk.

        """
        if not isinstance(delimiter, basestring):
            raise StreamError('`read_until` can only accept `str` param')
        self._delimiter = delimiter
        self._include_delimiter = include

        self._add_callback(callback)
        self._process_read()

    def _process_read(self):
        """fd -> read buffer -> memory"""
        while not self.closed:
            if self._to_read_buffer() == 0:
                # End of read
                break
        if not self._read_buffer:
            self._attach_read_handler()
            return

        self._read()

    def _to_read_buffer(self):
        """Read chunk from socket or file and returns number of bytes read.

        """
        try:
            chunk = self._read_from_fd()
            if not chunk or chunk is None:
                return 0
        except socket.error as e:
            self.close()
            return

        # No buffer size limit yet.
        self._read_buffer.append(chunk)
        self._read_buffer_bytes += len(chunk)
        return len(chunk)

    def _read(self):
        """Read chunk from `_read_buffer` and run callback with that chunk.

        """
        self._raise_if_closed()

        # XXX: handle all expectable cases
        read_bytes = 0
        if self._bytes_to_read is not None:
            read_bytes = min(self._bytes_to_read, self._read_buffer_bytes)
            self._run_callback(
                self._pop_callback(), self._pop_chunk(read_bytes))

        if self._delimiter is not None:
            while True:
                if not self._read_buffer:
                    break

                pos = self._read_buffer[0].find(self._delimiter)
                if pos != -1:
                    # Found delimiter
                    if self._include_delimiter:
                        pos += len(self._delimiter)
                    self._delimiter = None
                    self._run_callback(
                        self._pop_callback(), self._pop_chunk(pos))
                    break

                if len(self._read_buffer) == 1:
                    # No delimiter found in whole read buffer.
                    break

                # No delimiter found in first chunk.
                self._read_buffer.gather(
                    len(self._read_buffer[0] + self._read_buffer[1]))

    def _pop_chunk(self, read_bytes):
        """Pop chunk from `_read_buffer` and Returns chunk."""
        self._read_buffer_bytes -= read_bytes
        self._read_buffer.gather(read_bytes)
        return self._read_buffer.popleft()

    def _read_from_fd(self):
        raise NotImplementedError()

    def _attach_read_handler(self):
        """Attach read handler to `looper`.
        If socket throws EWOULDBLOCK with empty `_read_buffer`,
        `looper` should observe that socket for lazy reading.

        """
        if self._handler_event is None:
            self._attach_stream_handler(PollEvents.READ)

    def write(self, chunk, callback):
        if not isinstance(chunk, basestring):
            raise StreamError('Can write only chunk of `bytes`')

        self._add_callback(callback, read=False)
        self._process_write(chunk)

    def _process_write(self, chunk, partial=False):
        self._raise_if_closed()

        if not partial:
            self._to_write_buffer(chunk)

        written = None
        while self._write_buffer:
            try:
                num_bytes = self._write_to_fd(self._write_buffer[0])
                if num_bytes == 0:
                    self._write_buffer.frozen = True
                    break
                self._write_buffer.frozen = False

                # Partial writing is handled here.
                self._write_buffer.gather(num_bytes)
                written = self._write_buffer.popleft()
            except socket.error as e:
                if e.args[0] in EWOULDBLOCK:
                    # Freeze
                    self._write_buffer.frozen = True
                elif e.args[0] in ECONNRESET:
                    self.close()
                else:
                    raise StreamError(e)
                break

        # Post write process.
        if self._write_buffer and not partial:
            # Writing is not completed at one go
            self._attach_stream_handler(PollEvents.WRITE)
        elif not self._write_buffer:
            self._run_callback(self._pop_callback(read=False), written)

    def _to_write_buffer(self, chunk):
        """Fill `_write_buffer` after dividing `chunk` with
        `_write_chunk_size` bytes blocks.

        """

        if chunk:
            for i in range(0, len(chunk), self._write_chunk_size):
                self._write_buffer.append(chunk[0:i + self._write_chunk_size])

    def _write_to_fd(self, chunk):
        raise NotImplementedError()

    def _raise_if_closed(self):
        if self.closed:
            raise StreamError('Cannot read because stream is already closed')

    def _pop_callback(self, read=True):
        """Returns saved callback and clear read or write callback

        @param read (optional): True if read callback else write callback.
        """
        if read:
            callback = self._read_callback
            self._read_callback = None
        else:
            callback = self._write_callback
            self._write_callback = None
        return callback

    def _add_callback(self, callback, read=True):
        """Check whether passed callback has acceptable form.
        We imported `inspect` here because it's not on the mainstream.

        @param callback: Callback method to be saved.
        @param read (optional): True if read callback else write callback.
        """
        from inspect import getargspec
        if callback is None:
            return

        if not hasattr(callback, '__call__'):
            raise StreamError('Stream callback is not callable')

        spec = getargspec(callback)
        if not spec.args:
            raise StreamError(
                'Should provide room for chunk in stream callback')

        if read:
            self._read_callback = callback
        else:
            self._write_callback = callback

    def _run_callback(self, callback, *args):
        """Immediately run saved callback"""
        try:
            if callback is not None:
                callback(*args)
        except Exception as e:
            self.close()
            # We don't wrap exception here because we don't know
            # what caused exception at all.
            raise

    def _run_close_callback(self):
        if self.closed:
            if self._close_callback is not None:
                callback = self._close_callback
                self._run_callback(callback)
                self._close_callback = None

    def event_handler(self, fd, events):
        """Handler which will attached to `looper`"""
        try:
            if events & PollEvents.READ:
                self._handle_read()

            if events & PollEvents.WRITE:
                self._handle_write()

            if events & PollEvents.ERROR:
                # TODO: Should handle PollEvents.ERROR
                pass

            if self.closed:
                return

        except Exception as e:
            raise StreamError(e)

    def _handle_write(self):
        """Handle write process when fd is available.
        This method will be passed to event handler of `looper`

        """
        try:
            self._process_write()
        except Exception:
            self.close()

    def _handle_read(self):
        """Handle read process when fd is available.
        This method will be passed to event handler of `looper`

        """
        try:
            self._process_read()
        except Exception:
            self.close()

    def _attach_stream_handler(self, event_mask):
        """Attach handler to `looper` for the purpose of handling
        asynchronous reading and writing"""
        if self._handler_event is None:
            # Attach new handler
            self._handler_event = event_mask | PollEvents.ERROR
            self._looper.attach_handler(
                self.fileno(), self._handler_event, self.event_handler)
        elif not self._handler_event & event_mask:
            # Update event of existing handler
            self._handler_event |= event_mask
            self._looper.update_handler(self.fileno(), event_mask)

class SocketStream(BaseStream):
    def __init__(self, socket_, *args, **kwargs):
        if not isinstance(socket_, socket.socket):
            raise StreamError(
                'SocketStream can only be initialized with `socket.socket`')
        self.socket = socket_
        super(SocketStream, self).__init__(*args, **kwargs)

    def fileno(self):
        return self.socket.fileno()

    def _read_from_fd(self):
        try:
            chunk = self.socket.recv(self._read_chunk_size)
            if not chunk:
                # Should close stream here because nothing is left to be read.
                self.close()
                return None
            return chunk
        except socket.error as e:
            if e.args[0] not in EWOULDBLOCK:
                raise
            return None

    def _write_to_fd(self, chunk):
        return self.socket.send(chunk)

    def _close_fd(self):
        self.socket.close()
        self.socket = None

class FileStream(BaseStream):
    def __init__(self, file_, *args, **kwargs):
        if not isinstance(file_, file):
            raise StreamError(
                'FileStream can only be initialized with `file`')
        self.file_ = file_
        super(FileStream, self).__init__(*args, **kwargs)

    def fileno(self):
        return self.file_.fileno()

    def _read_from_fd(self):
        return self.file_.read(self._read_chunk_size)

    def _write_to_fd(self, chunk):
        try:
            self.file_.write(chunk)
        except IOError as e:
            raise StreamError(e)

    def _close_fd(self):
        self.file_.close()
        self.file_ = None

<|endoftext|>