# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import datetime, timedelta
from odoo.tools import html2plaintext

from odoo import Command
from odoo.tests import Form, tagged
from odoo.exceptions import AccessError
from odoo.addons.stock.tests.test_report import TestReportsCommon
from odoo.addons.sale.tests.common import TestSaleCommon


class TestSaleStockReports(TestReportsCommon):
    def test_report_forecast_1_sale_order_replenishment(self):
        """ Create and confirm two sale orders: one for the next week and one
        for tomorrow. Then check in the report it's the most urgent who is
        linked to the qty. on stock.
        """
        # make sure first picking doesn't auto-assign
        self.picking_type_out.reservation_method = 'manual'

        today = datetime.today()
        # Put some quantity in stock.
        quant_vals = {
            'product_id': self.product.id,
            'product_uom_id': self.product.uom_id.id,
            'location_id': self.stock_location.id,
            'quantity': 5,
            'reserved_quantity': 0,
        }
        self.env['stock.quant'].create(quant_vals)
        # Create a first SO for the next week.
        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner
        # so_form.validity_date = today + timedelta(days=7)
        with so_form.order_line.new() as so_line:
            so_line.product_id = self.product
            so_line.product_uom_qty = 5
        so_1 = so_form.save()
        so_1.action_confirm()
        so_1.picking_ids.scheduled_date = today + timedelta(days=7)

        # Create a second SO for tomorrow.
        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner
        # so_form.validity_date = today + timedelta(days=1)
        with so_form.order_line.new() as so_line:
            so_line.product_id = self.product
            so_line.product_uom_qty = 5
        so_2 = so_form.save()
        so_2.action_confirm()
        so_2.picking_ids.scheduled_date = today + timedelta(days=1)

        report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
        self.assertEqual(len(lines), 2)
        line_1 = lines[0]
        line_2 = lines[1]
        self.assertEqual(line_1['quantity'], 5)
        self.assertTrue(line_1['replenishment_filled'])
        self.assertEqual(line_1['document_out']['id'], so_2.id)
        self.assertEqual(line_2['quantity'], 5)
        self.assertEqual(line_2['replenishment_filled'], False)
        self.assertEqual(line_2['document_out']['id'], so_1.id)

    def test_report_forecast_2_report_line_corresponding_to_so_line_highlighted(self):
        """ When accessing the report from a SO line, checks if the correct SO line is highlighted in the report
        """
        # We create 2 identical SO
        so_form = Form(self.env['sale.order'])
        so_form.partner_id = self.partner
        with so_form.order_line.new() as line:
            line.product_id = self.product
            line.product_uom_qty = 5
        so1 = so_form.save()
        so1.action_confirm()
        so2 = so1.copy()
        so2.action_confirm()

        # Check for both SO if the highlight (is_matched) corresponds to the correct SO
        for so in [so1, so2]:
            context = {"move_to_match_ids": so.order_line.move_ids.ids}
            _, _, lines = self.get_report_forecast(product_template_ids=self.product_template.ids, context=context)
            for line in lines:
                if line['document_out']['id'] == so.id:
                    self.assertTrue(line['is_matched'], "The corresponding SO line should be matched in the forecast report.")
                else:
                    self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the SO shoud not be matched.")

    def test_report_forecast_3_unreserve_2_step_delivery(self):
        """
        Check that the forecast correctly reconciles the outgoing moves
        that are part of a chain with stock availability when unreserved.
        """
        warehouse = self.env.ref("stock.warehouse0")
        warehouse.delivery_steps = 'pick_ship'
        product = self.product
        # Put 5 units in stock
        self.env['stock.quant']._update_available_quantity(product, warehouse.lot_stock_id, 5)
        # Create and confirm an SO for 3 units
        so = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'name': product.name,
                    'product_id': product.id,
                    'product_uom_qty': 3,
                }),
            ],
        })
        so.action_confirm()
        _, _, lines = self.get_report_forecast(product_template_ids=product.product_tmpl_id.ids)
        outgoing_line = next(filter(lambda line: line.get('document_out'), lines))
        self.assertEqual(
            (outgoing_line['document_out']['id'], outgoing_line['quantity'], outgoing_line['replenishment_filled'], outgoing_line['reservation']['id']),
            (so.id, 3.0, True, so.picking_ids.filtered(lambda p: p.picking_type_id == warehouse.pick_type_id).id)
        )
        stock_line = next(filter(lambda line: not line.get('document_out'), lines))
        self.assertEqual(
            (stock_line['quantity'], stock_line['replenishment_filled'], stock_line['reservation']),
            (2.0, True, False)
        )
        # unrerseve the PICK delivery
        pick_delivery = so.picking_ids.filtered(lambda p: p.picking_type_id == warehouse.pick_type_id)
        pick_delivery.do_unreserve()
        _, _, lines = self.get_report_forecast(product_template_ids=product.product_tmpl_id.ids)
        outgoing_line = next(filter(lambda line: line.get('document_out'), lines))
        self.assertEqual(
            (outgoing_line['document_out']['id'], outgoing_line['quantity'], outgoing_line['replenishment_filled'], outgoing_line['reservation']),
            (so.id, 3.0, True, False)
        )
        stock_line = next(filter(lambda line: not line.get('document_out'), lines))
        self.assertEqual(
            (stock_line['quantity'], stock_line['replenishment_filled'], stock_line['reservation']),
            (2.0, True, False)
        )

    def test_report_forecast_4_so_from_another_salesman(self):
        """ Try accessing the forecast with a user that has only access to his SO while another user has created:
            - A draft Sale Order
            - A confirmed Sale Order
            The report shoud be usable by that user, and while he cannot open those SO, he should still see them to have the correct
            informations in the report.
        """
        # Create the SO & confirm it with first user
        with Form(self.env['sale.order']) as so_form:
            so_form.partner_id = self.partner
            with so_form.order_line.new() as line:
                line.product_id = self.product
                line.product_uom_qty = 3
            sale_order = so_form.save()
        sale_order.action_confirm()

        # Create a draft SO with the same user for the same product
        with Form(self.env['sale.order']) as so_form:
            so_form.partner_id = self.partner
            with so_form.order_line.new() as line:
                line.product_id = self.product
                line.product_uom_qty = 2
            draft = so_form.save()

        # Create second user which only has access to its own documents
        other = self.env['res.users'].create({
            'name': 'Other Salesman',
            'login': 'other',
            'groups_id': [
                Command.link(self.env.ref('sales_team.group_sale_salesman').id),
                Command.link(self.env.ref('stock.group_stock_user').id),
            ],
        })

        # Need to reset the cache otherwise it wouldn't trigger an Access Error anyway as the Sale Order is already there.
        sale_order.env.invalidate_all()
        report_values = self.env['stock.forecasted_product_product'].with_user(other).get_report_values(docids=self.product.ids)
        self.assertEqual(len(report_values['docs']['lines']), 1)
        self.assertEqual(report_values['docs']['lines'][0]['document_out']['name'], sale_order.name)
        self.assertEqual(len(report_values['docs']['draft_sale_orders']), 1)
        self.assertEqual(report_values['docs']['draft_sale_orders'][0]['name'], draft.name)

        # While 'other' can see these SO on the report, they shouldn't be able to access them.
        with self.assertRaises(AccessError):
            sale_order.with_user(other).check_access('read')
        with self.assertRaises(AccessError):
            draft.with_user(other).check_access('read')


@tagged('post_install', '-at_install')
class TestSaleStockInvoices(TestSaleCommon):

    def setUp(self):
        super(TestSaleStockInvoices, self).setUp()
        self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('stock.group_production_lot').id)]})
        self.product_by_lot = self.env['product.product'].create({
            'name': 'Product By Lot',
            'is_storable': True,
            'tracking': 'lot',
        })
        self.product_by_usn = self.env['product.product'].create({
            'name': 'Product By USN',
            'is_storable': True,
            'tracking': 'serial',
        })
        self.warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
        self.stock_location = self.warehouse.lot_stock_id
        lot = self.env['stock.lot'].create({
            'name': 'LOT0001',
            'product_id': self.product_by_lot.id,
        })
        self.usn01 = self.env['stock.lot'].create({
            'name': 'USN0001',
            'product_id': self.product_by_usn.id,
        })
        self.usn02 = self.env['stock.lot'].create({
            'name': 'USN0002',
            'product_id': self.product_by_usn.id,
        })
        self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 10, lot_id=lot)
        self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=self.usn01)
        self.env['stock.quant']._update_available_quantity(self.product_by_usn, self.stock_location, 1, lot_id=self.usn02)

    def test_invoice_less_than_delivered(self):
        """
        Suppose the lots are printed on the invoices.
        A user invoice a tracked product with a smaller quantity than delivered.
        On the invoice, the quantity of the used lot should be the invoiced one.
        """
        display_lots = self.env.ref('stock_account.group_lot_on_invoice')
        display_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 5}),
            ],
        })
        so.action_confirm()

        picking = so.picking_ids
        picking.move_ids.write({'quantity': 5, 'picked': True})
        picking.button_validate()

        invoice = so._create_invoices()
        with Form(invoice) as form:
            with form.invoice_line_ids.edit(0) as line:
                line.quantity = 2
        invoice.action_post()

        html = self.env['ir.actions.report']._render_qweb_html(
            'account.report_invoice_with_payments', invoice.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By Lot\n2.00Units\nLOT0001', "There should be a line that specifies 2 x LOT0001")

    def test_invoice_before_delivery(self):
        """
        Suppose the lots are printed on the invoices.
        The user sells a tracked product, its invoicing policy is "Ordered quantities"
        A user invoice a tracked product with a smaller quantity than delivered.
        On the invoice, the quantity of the used lot should be the invoiced one.
        """
        display_lots = self.env.ref('stock_account.group_lot_on_invoice')
        display_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})

        self.product_by_lot.invoice_policy = "order"

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 4}),
            ],
        })
        so.action_confirm()

        invoice = so._create_invoices()
        invoice.action_post()

        picking = so.picking_ids
        picking.move_ids.write({'quantity': 4, 'picked': True})
        picking.button_validate()

        html = self.env['ir.actions.report']._render_qweb_html(
            'account.report_invoice_with_payments', invoice.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By Lot\n4.00Units\nLOT0001', "There should be a line that specifies 4 x LOT0001")

    def test_backorder_and_several_invoices(self):
        """
        Suppose the lots are printed on the invoices.
        The user sells 2 tracked-by-usn products, he delivers 1 product and invoices it
        Then, he delivers the other one and invoices it too. Each invoice should have the
        correct USN
        """
        display_lots = self.env.ref('stock_account.group_lot_on_invoice')
        display_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}),
            ],
        })
        so.action_confirm()

        picking = so.picking_ids
        picking.move_ids.move_line_ids[0].quantity = 1
        picking.button_validate()

        invoice01 = so._create_invoices()
        with Form(invoice01) as form:
            with form.invoice_line_ids.edit(0) as line:
                line.quantity = 1
        invoice01.action_post()

        backorder = picking.backorder_ids
        backorder.move_ids.move_line_ids.quantity = 1
        backorder.button_validate()

        IrActionsReport = self.env['ir.actions.report']
        html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
        self.assertNotIn('USN0002', text)

        invoice02 = so._create_invoices()
        invoice02.action_post()
        html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice02.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")
        self.assertNotIn('USN0001', text)

        # Posting the second invoice shouldn't change the result of the first one
        html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should still be a line that specifies 1 x USN0001")
        self.assertNotIn('USN0002', text)

        # Resetting and posting again the first invoice shouldn't change the results
        invoice01.button_draft()
        invoice01.action_post()
        html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should still be a line that specifies 1 x USN0001")
        self.assertNotIn('USN0002', text)
        html = IrActionsReport._render_qweb_html('account.report_invoice_with_payments', invoice02.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")
        self.assertNotIn('USN0001', text)

    def test_invoice_with_several_returns(self):
        """
        Mix of returns and partial invoice
        - Product P tracked by lot
        - SO with 10 x P
        - Deliver 10 x Lot01
        - Return 10 x Lot01
        - Deliver 03 x Lot02
        - Invoice 02 x P
        - Deliver 05 x Lot02 + 02 x Lot03
        - Invoice 08 x P
        """
        display_lots = self.env.ref('stock_account.group_lot_on_invoice')
        display_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})

        lot01 = self.env['stock.lot'].search([('name', '=', 'LOT0001')])
        lot02, lot03 = self.env['stock.lot'].create([{
            'name': name,
            'product_id': self.product_by_lot.id,
        } for name in ['LOT0002', 'LOT0003']])
        self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 8, lot_id=lot02)
        self.env['stock.quant']._update_available_quantity(self.product_by_lot, self.stock_location, 2, lot_id=lot03)

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'name': self.product_by_lot.name, 'product_id': self.product_by_lot.id, 'product_uom_qty': 10}),
            ],
        })
        so.action_confirm()

        # Deliver 10 x LOT0001
        delivery01 = so.picking_ids
        delivery01.move_ids.write({'quantity': 10, 'picked': True})
        delivery01.button_validate()
        self.assertEqual(delivery01.move_line_ids.lot_id.name, 'LOT0001')

        # Return delivery01 (-> 10 x LOT0001)
        return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[delivery01.id], active_id=delivery01.id, active_model='stock.picking'))
        return_wizard = return_form.save()
        return_wizard.product_return_moves.quantity = 10
        action = return_wizard.action_create_returns()
        pick_return = self.env['stock.picking'].browse(action['res_id'])

        move_form = Form(pick_return.move_ids, view='stock.view_stock_move_operations')
        with move_form.move_line_ids.edit(0) as line:
            line.lot_id = lot01
            line.quantity = 10
        move_form.save()
        pick_return.move_ids.picked = True
        pick_return.button_validate()

        # Return pick_return
        return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[pick_return.id], active_id=pick_return.id, active_model='stock.picking'))
        return_wizard = return_form.save()
        return_wizard.product_return_moves.quantity = 10
        action = return_wizard.action_create_returns()
        delivery02 = self.env['stock.picking'].browse(action['res_id'])

        # Deliver 3 x LOT0002
        delivery02.do_unreserve()
        move_form = Form(delivery02.move_ids, view='stock.view_stock_move_operations')
        with move_form.move_line_ids.new() as line:
            line.lot_id = lot02
            line.quantity = 3
        move_form.save()
        delivery02.move_ids.picked = True
        Form.from_action(self.env, delivery02.button_validate()).save().process()

        # Invoice 2 x P
        invoice01 = so._create_invoices()
        with Form(invoice01) as form:
            with form.invoice_line_ids.edit(0) as line:
                line.quantity = 2
        invoice01.action_post()

        html = self.env['ir.actions.report']._render_qweb_html(
            'account.report_invoice_with_payments', invoice01.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By Lot\n2.00Units\nLOT0002', "There should be a line that specifies 2 x LOT0002")
        self.assertNotIn('LOT0001', text)

        # Deliver 5 x LOT0002 + 2 x LOT0003
        delivery03 = delivery02.backorder_ids
        delivery03.do_unreserve()
        move_form = Form(delivery03.move_ids, view='stock.view_stock_move_operations')
        with move_form.move_line_ids.new() as line:
            line.lot_id = lot02
            line.quantity = 5
        with move_form.move_line_ids.new() as line:
            line.lot_id = lot03
            line.quantity = 2
        move_form.save()
        delivery03.move_ids.picked = True
        delivery03.button_validate()

        # Invoice 8 x P
        invoice02 = so._create_invoices()
        invoice02.action_post()

        html = self.env['ir.actions.report']._render_qweb_html(
            'account.report_invoice_with_payments', invoice02.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By Lot\n6.00Units\nLOT0002', "There should be a line that specifies 6 x LOT0002")
        self.assertRegex(text, r'Product By Lot\n2.00Units\nLOT0003', "There should be a line that specifies 2 x LOT0003")
        self.assertNotIn('LOT0001', text)

    def test_refund_cancel_invoices(self):
        """
        Suppose the lots are printed on the invoices.
        The user sells 2 tracked-by-usn products, he delivers 2 products and invoices them
        Then he adds credit notes and issues a full refund. Receive the products.
        The reversed invoice should also have correct USN
        """
        display_lots = self.env.ref('stock_account.group_lot_on_invoice')
        display_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 2}),
            ],
        })
        so.action_confirm()

        picking = so.picking_ids
        picking.move_ids.move_line_ids[0].quantity = 1
        picking.move_ids.move_line_ids[1].quantity = 1
        picking.move_ids.picked = True
        picking.button_validate()

        invoice01 = so._create_invoices()
        invoice01.action_post()

        html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")

        # Refund the invoice
        refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
            'journal_id': invoice01.journal_id.id,
        })
        res = refund_wizard.refund_moves()
        refund_invoice = self.env['account.move'].browse(res['res_id'])
        refund_invoice.action_post()

        # recieve the returned product
        stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(active_ids=picking.ids, active_id=picking.sorted().ids[0], active_model='stock.picking'))
        return_wiz = stock_return_picking_form.save()
        return_wiz.product_return_moves.quantity = 2
        res = return_wiz.action_create_returns()
        pick_return = self.env['stock.picking'].browse(res['res_id'])

        move_form = Form(pick_return.move_ids, view='stock.view_stock_move_operations')
        with move_form.move_line_ids.edit(0) as line:
            line.lot_id = self.usn01
            line.quantity = 1
        with move_form.move_line_ids.edit(1) as line:
            line.lot_id = self.usn02
            line.quantity = 1
        move_form.save()
        pick_return.move_ids.picked = True
        pick_return.button_validate()

        # reversed invoice
        html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', refund_invoice.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0002', "There should be a line that specifies 1 x USN0002")

    def test_refund_modify_invoices(self):
        """
        Suppose the lots are printed on the invoices.
        The user sells 1 tracked-by-usn products, he delivers 1 and invoices it
        Then he adds credit notes and issues full refund and new draft invoice.
        The new draft invoice should have correct USN
        """
        display_lots = self.env.ref('stock_account.group_lot_on_invoice')
        display_uom = self.env.ref('uom.group_uom')
        self.env.user.write({'groups_id': [(4, display_lots.id), (4, display_uom.id)]})

        so = self.env['sale.order'].create({
            'partner_id': self.partner_a.id,
            'order_line': [
                (0, 0, {'name': self.product_by_usn.name, 'product_id': self.product_by_usn.id, 'product_uom_qty': 1}),
            ],
        })
        so.action_confirm()

        picking = so.picking_ids
        picking.move_ids.move_line_ids[0].quantity = 1
        picking.move_ids.picked = True
        picking.button_validate()

        invoice01 = so._create_invoices()
        invoice01.action_post()

        html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', invoice01.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")

        # Refund the invoice with full refund and new draft invoice
        refund_wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
            'journal_id': invoice01.journal_id.id,
        })
        res = refund_wizard.modify_moves()
        invoice02 = self.env['account.move'].browse(res['res_id'])
        invoice02.action_post()

        # new draft invoice
        html = self.env['ir.actions.report']._render_qweb_html('account.report_invoice_with_payments', invoice02.ids)[0]
        text = html2plaintext(html)
        self.assertRegex(text, r'Product By USN\n1.00Units\nUSN0001', "There should be a line that specifies 1 x USN0001")
