# Copyright 2017 Eficent Business and IT Consulting Services S.L.
#   (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests import common
from odoo.exceptions import ValidationError
from odoo.exceptions import AccessError

from datetime import datetime, timedelta


class TestStockCycleCount(common.TransactionCase):

    def setUp(self):
        super(TestStockCycleCount, self).setUp()
        self.res_users_model = self.env['res.users']
        self.cycle_count_model = self.env['stock.cycle.count']
        self.stock_cycle_count_rule_model = self.env['stock.cycle.count.rule']
        self.inventory_model = self.env['stock.inventory']
        self.stock_location_model = self.env['stock.location']
        self.stock_move_model = self.env['stock.move']
        self.stock_warehouse_model = self.env['stock.warehouse']
        self.product_model = self.env['product.product']
        self.quant_model = self.env['stock.quant']
        self.move_model = self.env['stock.move']

        self.company = self.env.ref('base.main_company')
        self.partner = self.env.ref('base.res_partner_1')
        self.g_stock_manager = self.env.ref('stock.group_stock_manager')
        self.g_stock_user = self.env.ref('stock.group_stock_user')

        # Create users:
        self.manager = self._create_user(
            'user_1', [self.g_stock_manager], self.company).id
        self.user = self._create_user(
            'user_2', [self.g_stock_user], self.company).id

        # Create warehouses:
        self.big_wh = self.stock_warehouse_model.create({
            'name': 'BIG',
            'code': 'B',
            'cycle_count_planning_horizon': 30})
        self.small_wh = self.stock_warehouse_model.create({
            'name': 'SMALL', 'code': 'S'})

        # Create rules:
        self.rule_periodic = \
            self._create_stock_cycle_count_rule_periodic(
                self.manager, 'rule_1', [2, 7])
        self.rule_turnover = \
            self._create_stock_cycle_count_rule_turnover(
                self.manager, 'rule_2', [100])
        self.rule_accuracy = \
            self._create_stock_cycle_count_rule_accuracy(
                self.manager, 'rule_3', [5], self.big_wh.view_location_id.ids)
        self.zero_rule = self._create_stock_cycle_count_rule_zero(
            self.manager, 'rule_4')

        # Configure warehouses:
        self.rule_ids = [
            self.rule_periodic.id,
            self.rule_turnover.id,
            self.rule_accuracy.id,
            self.zero_rule.id]
        self.big_wh.write({
            'cycle_count_rule_ids': [(6, 0, self.rule_ids)]
        })

        # Create a location:
        self.count_loc = self.stock_location_model.create({
            'name': 'Place',
            'usage': 'production'
        })
        self.stock_location_model._parent_store_compute()

        # Create a cycle count:
        self.cycle_count_1 = self.cycle_count_model.sudo(self.manager).create({
            'name': 'Test cycle count',
            'cycle_count_rule_id': self.rule_periodic.id,
            'location_id': self.count_loc.id,
        })

        # Create a product:
        self.product1 = self.product_model.create({
            'name': 'Test Product 1',
            'type': 'product',
            'default_code': 'PROD1',
        })

    def _create_user(self, login, groups, company):
        group_ids = [group.id for group in groups]
        user = self.res_users_model.create({
            'name': login,
            'login': login,
            'email': 'example@yourcompany.com',
            'company_id': company.id,
            'company_ids': [(4, company.id)],
            'groups_id': [(6, 0, group_ids)]
        })
        return user

    def _create_stock_cycle_count_rule_periodic(self, uid, name, values):
        rule = self.stock_cycle_count_rule_model.sudo(uid).create({
            'name': name,
            'rule_type': 'periodic',
            'periodic_qty_per_period': values[0],
            'periodic_count_period': values[1],
        })
        return rule

    def _create_stock_cycle_count_rule_turnover(self, uid, name, values):
        rule = self.stock_cycle_count_rule_model.sudo(uid).create({
            'name': name,
            'rule_type': 'turnover',
            'turnover_inventory_value_threshold': values[0],
        })
        return rule

    def _create_stock_cycle_count_rule_accuracy(
            self, uid, name, values, zone_ids):
        rule = self.stock_cycle_count_rule_model.sudo(uid).create({
            'name': name,
            'rule_type': 'accuracy',
            'accuracy_threshold': values[0],
            'apply_in': 'location',
            'location_ids': [(6, 0, zone_ids)],
        })
        return rule

    def _create_stock_cycle_count_rule_zero(self, uid, name):
        rule = self.stock_cycle_count_rule_model.sudo(uid).create({
            'name': name,
            'rule_type': 'zero',
        })
        return rule

    def test_cycle_count_planner(self):
        """Tests creation of cycle counts."""
        # Common rules:
        wh = self.big_wh
        locs = self.stock_location_model
        for rule in self.big_wh.cycle_count_rule_ids:
            locs += wh._search_cycle_count_locations(rule)
        locs = locs.exists()  # remove duplicated locations.
        counts = self.cycle_count_model.search([
            ('location_id', 'in', locs.ids)])
        self.assertFalse(
            counts, 'Existing cycle counts before execute planner.')
        date_pre_existing_cc = datetime.today() + timedelta(days=30)
        loc = locs.filtered(lambda l: l.usage != 'view')[0]
        pre_existing_count = self.cycle_count_model.create({
            'name': 'To be cancelled when running cron job.',
            'cycle_count_rule_id': self.rule_periodic.id,
            'location_id': loc.id,
            'date_deadline': date_pre_existing_cc
        })
        self.assertEqual(pre_existing_count.state, 'draft',
                         'Testing data not generated properly.')
        date = datetime.today() - timedelta(days=1)
        self.inventory_model.create({
            'name': 'Pre-existing inventory',
            'location_id': loc.id,
            'date': date
        })
        self.quant_model.create({
            'product_id': self.product1.id,
            'location_id': self.count_loc.id,
            'quantity': 1.0,
        })
        move1 = self.stock_move_model.create({
            'name': 'Pre-existing move',
            'product_id': self.product1.id,
            'product_uom_qty': 1.0,
            'product_uom': self.product1.uom_id.id,
            'location_id': self.count_loc.id,
            'location_dest_id': loc.id
        })
        move1._action_confirm()
        move1._action_assign()
        move1.move_line_ids[0].qty_done = 1.0
        move1._action_done()
        wh.cron_cycle_count()
        self.assertNotEqual(pre_existing_count.date_deadline,
                            date_pre_existing_cc,
                            'Date of pre-existing cycle counts has not been '
                            'updated.')
        counts = self.cycle_count_model.search([
            ('location_id', 'in', locs.ids)])
        self.assertTrue(counts, 'Cycle counts not planned')
        # Zero-confirmations:
        count = self.cycle_count_model.search([
            ('location_id', '=', loc.id),
            ('cycle_count_rule_id', '=', self.zero_rule.id)])
        self.assertFalse(
            count, 'Unexpected zero confirmation.')
        move2 = self.move_model.create({
            'name': 'make the locations to run out of stock.',
            'product_id': self.product1.id,
            'product_uom_qty': 1.0,
            'product_uom': self.product1.uom_id.id,
            'location_id': loc.id,
            'location_dest_id': self.count_loc.id
        })
        move2._action_confirm()
        move2._action_assign()
        move2.move_line_ids[0].qty_done = 1.0
        move2._action_done()
        count = self.cycle_count_model.search([
            ('location_id', '=', loc.id),
            ('cycle_count_rule_id', '=', self.zero_rule.id)])
        self.assertTrue(
            count, 'Zero confirmation not being created.')

    def test_cycle_count_workflow(self):
        """Tests workflow."""
        self.cycle_count_1.action_create_inventory_adjustment()
        inventory = self.inventory_model.search([
            ('cycle_count_id', '=', self.cycle_count_1.id)])
        self.assertTrue(inventory, 'Inventory not created.')
        inventory.action_start()
        inventory.action_validate()
        self.assertEqual(self.cycle_count_1.state, 'done',
                         'Cycle count not set as done.')
        self.cycle_count_1.do_cancel()
        self.assertEqual(self.cycle_count_1.state, 'cancelled',
                         'Cycle count not set as cancelled.')

    def test_view_methods(self):
        """Tests the methods used to handle views."""
        self.cycle_count_1.action_create_inventory_adjustment()
        self.cycle_count_1.action_view_inventory()
        inv_count = self.cycle_count_1.inventory_adj_count
        self.assertEqual(inv_count, 1,
                         'View method failing.')
        rules = [self.rule_periodic,
                 self.rule_turnover,
                 self.rule_accuracy,
                 self.zero_rule]
        for r in rules:
            r._compute_rule_description()
            self.assertTrue(r.rule_description, 'No description provided')
        self.rule_accuracy._onchange_locaton_ids()
        self.assertEqual(self.rule_accuracy.warehouse_ids.ids, self.big_wh.ids,
                         'Rules defined for zones are not getting the right '
                         'warehouse.')

    def test_user_security(self):
        """Tests user rights."""
        with self.assertRaises(AccessError):
            self._create_stock_cycle_count_rule_periodic(
                self.user, 'rule_1b', [2, 7])
        with self.assertRaises(AccessError):
            self.cycle_count_1.sudo(self.user).unlink()

    def test_rule_periodic_constrains(self):
        """Tests the constrains for the periodic rules."""
        # constrain: periodic_qty_per_period < 1
        with self.assertRaises(ValidationError):
            self._create_stock_cycle_count_rule_periodic(
                self.manager, 'rule_0', [0, 0])
        # constrain: periodic_count_period < 0
        with self.assertRaises(ValidationError):
            self._create_stock_cycle_count_rule_periodic(
                self.manager, 'rule_0', [1, -1])

    def test_rule_zero_constrains(self):
        """Tests the constrains for the zero-confirmation rule: it might
        only exist one zero confirmation rule per warehouse and have just
        one warehouse assigned.
        """
        zero2 = self._create_stock_cycle_count_rule_zero(
            self.manager, 'zero_rule_2')
        with self.assertRaises(ValidationError):
            zero2.warehouse_ids = [(4, self.big_wh.id)]
        with self.assertRaises(ValidationError):
            self.zero_rule.warehouse_ids = [
                (4, self.small_wh.id)]
