779 lines
33 KiB
Python
779 lines
33 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
|
|
|
from datetime import datetime
|
|
from odoo import Command
|
|
from odoo.addons.industry_fsm_sale.tests.common import TestFsmFlowSaleCommon
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import tagged, Form, new_test_user
|
|
from odoo.tools.float_utils import float_compare
|
|
|
|
|
|
@tagged('-at_install', 'post_install')
|
|
class TestFsmFlowSale(TestFsmFlowSaleCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.task.partner_id = cls.partner_1
|
|
|
|
def test_invoicing_flow(self):
|
|
self.service_product_ordered.write({
|
|
'type': 'service',
|
|
'service_policy': 'ordered_prepaid',
|
|
})
|
|
self.service_product_delivered.write({
|
|
'type': 'service',
|
|
'service_policy': 'delivered_timesheet',
|
|
})
|
|
|
|
self.fsm_project.write({
|
|
'timesheet_product_id': self.service_product_delivered.id,
|
|
})
|
|
|
|
self.assertFalse(self.task.sale_order_id)
|
|
self.assertFalse(self.task.sale_line_id)
|
|
self.assertFalse(self.task.task_to_invoice)
|
|
self.service_product_ordered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).set_fsm_quantity(1.0)
|
|
self.assertEqual(len(self.task.sale_order_id.order_line), 1)
|
|
|
|
first_order_line = self.task.sale_order_id.order_line
|
|
self.assertEqual(first_order_line.product_uom_qty, 1.0)
|
|
self.assertFalse(self.task.display_create_invoice_primary)
|
|
self.assertFalse(self.task.sale_line_id)
|
|
|
|
self.task.sale_order_id.write({
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.service_timesheet.id,
|
|
'product_uom_qty': 1.0,
|
|
'name': '/',
|
|
}),
|
|
]
|
|
})
|
|
|
|
if self.task.sale_order_id.state != 'sale':
|
|
self.task.sale_order_id.action_confirm()
|
|
self.assertEqual(len(self.task.sale_order_id.order_line), 2)
|
|
service_timesheet_order_line = self.task.sale_order_id.order_line.filtered(lambda order_line: order_line.product_id == self.service_timesheet)
|
|
self.task.write({
|
|
'timesheet_ids': [
|
|
Command.create({
|
|
'name': '/',
|
|
'unit_amount': 0.5,
|
|
'employee_id': self.employee_user2.id,
|
|
'project_id': self.task.project_id.id,
|
|
}),
|
|
Command.create({
|
|
'name': '/',
|
|
'unit_amount': 0.5,
|
|
'employee_id': self.employee_user2.id,
|
|
'project_id': self.task.project_id.id,
|
|
}),
|
|
Command.create({
|
|
'name': '/',
|
|
'unit_amount': 1.0,
|
|
'so_line': service_timesheet_order_line.id,
|
|
'is_so_line_edited': True,
|
|
'employee_id': self.employee_user2.id,
|
|
'project_id': self.task.project_id.id,
|
|
}),
|
|
]
|
|
})
|
|
|
|
self.task.action_fsm_validate()
|
|
self.assertEqual(len(self.task.timesheet_ids.so_line), 2)
|
|
self.assertEqual(self.task.sale_order_id.order_line.mapped('qty_delivered'), [1.0] * 3)
|
|
self.assertEqual(self.task.sale_line_id.product_id, self.service_product_delivered)
|
|
self.assertEqual(self.task.sale_order_id.state, 'sale')
|
|
self.assertEqual(len(self.task.sale_order_id.order_line), 3)
|
|
second_order_line = self.task.sale_line_id
|
|
self.assertEqual(second_order_line.project_id, self.fsm_project)
|
|
self.assertEqual(second_order_line.task_id, self.task)
|
|
self.assertTrue(second_order_line.is_service)
|
|
self.assertEqual(second_order_line.qty_delivered_method, 'timesheet')
|
|
|
|
self.assertTrue(self.task.task_to_invoice)
|
|
self.assertTrue(self.task.display_create_invoice_primary)
|
|
self.task.sale_order_id._create_invoices()
|
|
self.assertEqual(self.task.invoice_count, 1)
|
|
self.assertFalse(self.task.display_create_invoice_primary)
|
|
|
|
self.service_product_ordered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.assertTrue(self.task.display_create_invoice_primary)
|
|
self.task.sale_order_id._create_invoices()
|
|
self.assertEqual(self.task.invoice_count, 2)
|
|
self.assertFalse(self.task.display_create_invoice_primary)
|
|
|
|
def test_invoice_fsm_task_with_diff_shipping_address(self):
|
|
"""
|
|
When the shipping address is different from the invoice address,
|
|
the task should be able to be invoiced once done.
|
|
"""
|
|
# activate setting for splitting the invoice and shipping address
|
|
config = self.env['res.config.settings'].create({
|
|
'group_sale_delivery_address': True,
|
|
})
|
|
config.execute()
|
|
fsm_product = self.env['product.product'].create({
|
|
'name': 'Fsm Product',
|
|
'type': 'service',
|
|
'list_price': 100,
|
|
'service_policy': 'ordered_prepaid',
|
|
'project_id': self.fsm_project.id,
|
|
'service_tracking': 'task_global_project',
|
|
})
|
|
billing_partner, shipping_partner = self.env['res.partner'].create([{
|
|
'name': 'Billing Partner',
|
|
}, {
|
|
'name': 'Shipping Partner',
|
|
}])
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': billing_partner.id,
|
|
'partner_invoice_id': billing_partner.id,
|
|
'partner_shipping_id': shipping_partner.id,
|
|
})
|
|
sale_order.order_line = self.env['sale.order.line'].create([{
|
|
'product_id': fsm_product.id,
|
|
'product_uom_qty': 1.0,
|
|
'order_id': sale_order.id,
|
|
}])
|
|
sale_order.action_confirm()
|
|
self.assertEqual(len(sale_order.tasks_ids), 1, "We should have 1 task after confirming the SO.")
|
|
task = sale_order.tasks_ids[0]
|
|
self.assertEqual(task.partner_id.commercial_partner_id, shipping_partner,
|
|
"Partner on the task should be the shipping address.")
|
|
self.assertEqual(task.sale_order_id, sale_order, "The sale order should be linked to the task.")
|
|
task.action_fsm_validate()
|
|
self.assertTrue(task.task_to_invoice, "Task should be invoiceable")
|
|
|
|
def test_task_sale_order_id_and_sale_order_line_id_consistency(self):
|
|
sale_order_1 = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_delivery_timesheet1.id,
|
|
'product_uom_qty': 10,
|
|
})
|
|
]
|
|
})
|
|
sale_order_1.action_confirm()
|
|
|
|
task = self.env['project.task'].with_context({
|
|
'default_project_id': self.fsm_project_employee_rate.id,
|
|
}).create({
|
|
'sale_line_id': sale_order_1.order_line.id,
|
|
'name': 'Test Task',
|
|
})
|
|
|
|
self.assertEqual(task.sale_order_id.id, sale_order_1.id)
|
|
|
|
sale_order_2 = sale_order_1.copy()
|
|
|
|
task.write({
|
|
'sale_line_id': sale_order_2.order_line.id,
|
|
})
|
|
|
|
self.assertEqual(task.sale_order_id.id, sale_order_2.id)
|
|
|
|
def test_qty_to_invoice_from_fsm(self):
|
|
"""
|
|
Check that sale order lines coming from an fsm task and for a product with a price of zero
|
|
are not going to be invoiced.
|
|
"""
|
|
product = self.product_a.with_context({'fsm_task_id': self.task.id})
|
|
product.set_fsm_quantity(2)
|
|
so = self.task.sale_order_id
|
|
if so.state == 'draft':
|
|
so.action_confirm() # needed if stock is not installed
|
|
sol = so.order_line[-1]
|
|
self.assertEqual(sol.qty_to_invoice, 2.0)
|
|
|
|
so_form = Form(so)
|
|
with so_form.order_line.edit(0) as line:
|
|
line.price_unit = 0.0
|
|
so_form.save()
|
|
self.assertEqual(sol.qty_to_invoice, 2.0, "Anglo Saxon Accounting should be enable")
|
|
|
|
with so_form.order_line.edit(0) as line:
|
|
line.price_unit = 0.01
|
|
so_form.save()
|
|
self.assertEqual(sol.qty_to_invoice, 2.0, "$0.01 shouldn't count as free")
|
|
self.env.company.anglo_saxon_accounting = False
|
|
with so_form.order_line.edit(0) as line:
|
|
line.price_unit = 0.00
|
|
so_form.save()
|
|
self.assertEqual(sol.qty_to_invoice, 0.0, "Anglo Saxon Accounting should be disable")
|
|
|
|
def test_qty_to_invoice_with_service_policy(self):
|
|
"""
|
|
Check that free service products with prepaid invoice policies are added to invoices, even
|
|
when Anglo-Saxon Accounting is not enabled
|
|
"""
|
|
self.task.partner_id = self.partner_1.id
|
|
self.env.company.anglo_saxon_accounting = False
|
|
self.service_product_delivered.list_price = 0.0
|
|
# Case 1: Service product is not invoiced while not on 'Prepaid' service_policy
|
|
self.service_product_delivered.service_policy = 'delivered_timesheet'
|
|
self.service_product_delivered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
so = self.task.sale_order_id
|
|
sol = so.order_line[0]
|
|
if so.state == 'draft':
|
|
so.action_confirm() # needed if stock is not installed
|
|
self.assertEqual(sol.qty_to_invoice, 0.0, "Qty to invoice should not be set for non-prepaid, free services (non-anglo-saxon)")
|
|
# Case 2: Service product is invoiced on 'Prepaid' service_policy
|
|
self.service_product_delivered.service_policy = 'ordered_prepaid'
|
|
self.service_product_delivered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.assertEqual(sol.qty_to_invoice, 2.0, "Qty to invoice should be set for prepaid, free services, even if Anglo-Saxon Accounting is disabled")
|
|
|
|
def test_uom_conversion_fsm_task_to_so(self):
|
|
"""Checks that the hours recorded on Timesheets are converted to the correct UOM on the Sales Order"""
|
|
|
|
quarter_hour = self.env['uom.uom'].create({
|
|
'name': 'Quarter-Hours',
|
|
'relative_factor': 0.25,
|
|
'relative_uom_id': self.env.ref('uom.product_uom_hour').id,
|
|
})
|
|
self.service_timesheet._inverse_service_policy() # trigger value changes for invoice policy and service_type
|
|
self.service_timesheet.uom_id = quarter_hour
|
|
self.service_timesheet.list_price = 40
|
|
self.fsm_project.sale_line_employee_ids = [Command.create({
|
|
'employee_id': self.employee_user2.id,
|
|
'timesheet_product_id': self.service_timesheet.id,
|
|
'price_unit': 40,
|
|
})]
|
|
field_task = self.env['project.task'].create({
|
|
'name': 'Field Task',
|
|
'project_id': self.fsm_project.id,
|
|
'timesheet_ids': [Command.create({
|
|
'employee_id': self.employee_user2.id,
|
|
'name': '/',
|
|
'unit_amount': 1.75, # 01:45
|
|
'product_uom_id': self.env['uom.uom'].search([('name', '=', 'Hours')]).id
|
|
})],
|
|
'partner_id': self.partner_1.id,
|
|
})
|
|
field_task.action_fsm_validate()
|
|
sale_order = field_task.sale_order_id
|
|
order_lines = sale_order.order_line
|
|
self.assertEqual(float_compare(order_lines.product_uom_qty, 7.0, precision_digits=2), 0, "The Ordered Quantities should match the Timesheets at the time of creation")
|
|
|
|
def test_prevent_empty_sol_for_timesheet(self):
|
|
"""
|
|
Test that when adding a product to the task and mark as done that task then,
|
|
it does not create an empty SOL for timesheet if no timesheets are added.
|
|
|
|
Test Case:
|
|
=========
|
|
1. Create a task and add a product to it
|
|
2. Validate the task
|
|
3. Check the sale order line should not be created for timesheet
|
|
"""
|
|
self.fsm_project.write({
|
|
'timesheet_product_id': self.service_timesheet.id,
|
|
})
|
|
self.service_product_ordered.with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.task.action_fsm_validate()
|
|
sale_order_lines = self.env['sale.order.line'].search([('order_id', '=', self.task.sale_order_id.id)])
|
|
self.assertEqual(len(sale_order_lines), 1)
|
|
self.assertEqual(sale_order_lines.product_id, self.service_product_ordered)
|
|
|
|
def test_task_sale_order_auto_fills_salesperson(self):
|
|
"""
|
|
Check that sale order has an assigned salesperson by default when adding products to it.
|
|
Expected behavior is: 1st assignee if any, current user if not
|
|
"""
|
|
assignee_1 = new_test_user(self.env, login='john')
|
|
assignee_2 = new_test_user(self.env, login='albert')
|
|
|
|
task_no_assignee, task_with_assignee = self.env['project.task'].with_context({
|
|
'default_project_id': self.fsm_project_employee_rate.id,
|
|
}).create([
|
|
{
|
|
'name': 'No assignee',
|
|
'partner_id': self.partner_1.id
|
|
},
|
|
{
|
|
'name': 'With assignee',
|
|
'partner_id': self.partner_1.id,
|
|
'user_ids': [assignee_1.id, assignee_2.id],
|
|
},
|
|
])
|
|
|
|
task_no_assignee._fsm_create_sale_order()
|
|
task_with_assignee._fsm_create_sale_order()
|
|
self.assertEqual(task_no_assignee.sale_order_id.user_id, self.env.user)
|
|
self.assertEqual(task_with_assignee.sale_order_id.user_id, assignee_1)
|
|
|
|
def test_invoice_status_after_change_price_for_fully_invoiced_order(self):
|
|
so = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.consu_product_ordered.id,
|
|
'product_uom_qty': 10,
|
|
})
|
|
]
|
|
})
|
|
|
|
sol = self.env['sale.order.line'].create({
|
|
'order_id': so.id,
|
|
'product_id': self.service_product_delivered.id,
|
|
'product_uom_qty': 3,
|
|
'price_unit': 40,
|
|
'sequence': 20,
|
|
'task_id': self.task.id
|
|
})
|
|
so.action_confirm()
|
|
sol.qty_delivered = 3
|
|
|
|
invoice = so._create_invoices()
|
|
invoice.action_post()
|
|
|
|
self.assertEqual(so.invoice_status, 'invoiced')
|
|
sol.price_unit = 0
|
|
self.assertEqual(so.invoice_status, 'invoiced')
|
|
|
|
def test_copy_task_sale_fields(self):
|
|
normal_project = self.env['project.project'].create({
|
|
'name': 'Normal Project',
|
|
'allow_billable': True,
|
|
'allow_timesheets': True,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
normal_task = self.env['project.task'].create({
|
|
'name': 'Original Task for Copy Test',
|
|
'project_id': normal_project.id,
|
|
'partner_id': self.partner_1.id,
|
|
'timesheet_ids': [Command.create({
|
|
'employee_id': self.employee_user2.id,
|
|
'name': '/',
|
|
'unit_amount': 1,
|
|
})],
|
|
})
|
|
|
|
fsm_product = self.env['product.product'].create({
|
|
'name': 'Fsm Product',
|
|
'list_price': 100,
|
|
})
|
|
fsm_task = self.env['project.task'].create({
|
|
'name': 'Original Task for Copy Test',
|
|
'project_id': self.fsm_project.id,
|
|
'partner_id': self.partner_1.id,
|
|
})
|
|
fsm_task_with_recurrence = self.env['project.task'].create({
|
|
'name': 'Original Task for Copy Test 2',
|
|
'project_id': self.fsm_project.id,
|
|
'partner_id': self.partner_1.id,
|
|
'recurring_task': True,
|
|
})
|
|
|
|
fsm_product.with_context({'fsm_task_id': fsm_task.id}).set_fsm_quantity(10.0)
|
|
fsm_product.with_context({'fsm_task_id': normal_task.id}).set_fsm_quantity(10.0)
|
|
fsm_product.with_context({'fsm_task_id': fsm_task_with_recurrence.id}).set_fsm_quantity(10.0)
|
|
|
|
self.assertTrue(fsm_task.sale_order_id)
|
|
self.assertEqual(len(fsm_task.sale_order_id.order_line), 1)
|
|
|
|
self.assertTrue(normal_task.sale_order_id)
|
|
self.assertEqual(len(normal_task.sale_order_id.order_line), 1)
|
|
|
|
self.assertTrue(fsm_task_with_recurrence.sale_order_id)
|
|
self.assertEqual(len(fsm_task_with_recurrence.sale_order_id.order_line), 1)
|
|
|
|
fsm_task.action_fsm_validate()
|
|
fsm_task.sale_order_id._create_invoices()
|
|
|
|
normal_task.action_fsm_validate()
|
|
normal_task.sale_order_id._create_invoices()
|
|
|
|
fsm_task_with_recurrence.action_fsm_validate()
|
|
fsm_task_with_recurrence.sale_order_id._create_invoices()
|
|
|
|
self.assertTrue(fsm_task.sale_order_id.invoice_ids)
|
|
self.assertTrue(normal_task.sale_order_id.invoice_ids)
|
|
self.assertTrue(fsm_task_with_recurrence.sale_order_id.invoice_ids)
|
|
|
|
fsm_task.update(fsm_task.copy_data()[0])
|
|
normal_task.update(normal_task.copy_data()[0])
|
|
fsm_task_with_recurrence.update(fsm_task_with_recurrence.copy_data()[0])
|
|
|
|
self.assertFalse(fsm_task.sale_order_id)
|
|
self.assertFalse(fsm_task.sale_order_id.invoice_ids)
|
|
|
|
self.assertTrue(normal_task.sale_order_id)
|
|
self.assertTrue(normal_task.sale_order_id.invoice_ids)
|
|
|
|
self.assertTrue(fsm_task_with_recurrence.sale_order_id)
|
|
self.assertTrue(fsm_task_with_recurrence.sale_order_id.invoice_ids)
|
|
|
|
def test_invoice_without_sale_order(self):
|
|
"""Should return a red toast notification if no sale order is linked."""
|
|
res = self.task.action_create_invoice()
|
|
self.assertEqual(res['params']['type'], 'danger', "Expected a red toast notification")
|
|
|
|
def test_invoice_status_so_non_anglo_saxon_task_anglo_saxon(self):
|
|
"""
|
|
When the SO's company has Anglo-Saxon accounting disabled and the
|
|
material price is zero, the invoice status must be 'invoiced'.
|
|
The task's company Anglo-Saxon setting must have no influence.
|
|
"""
|
|
self.env.company.anglo_saxon_accounting = False
|
|
self.company_data_2['company'].anglo_saxon_accounting = True
|
|
fsm_project_second = self.env['project.project'].create({
|
|
'name': 'Field Service second',
|
|
'is_fsm': True,
|
|
'company_id': self.company_data_2['company'].id,
|
|
})
|
|
task_second = self.env['project.task'].create({
|
|
'name': 'Fsm task second',
|
|
'project_id': fsm_project_second.id,
|
|
'partner_id': self.partner_1.id,
|
|
})
|
|
self.assertTrue(task_second.company_id.anglo_saxon_accounting)
|
|
task_second_so = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'project_id': fsm_project_second.id,
|
|
'task_id': task_second.id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
task_second.sale_order_id = task_second_so
|
|
self.assertFalse(task_second_so.company_id.anglo_saxon_accounting)
|
|
|
|
product = self.env['product.product'].create({
|
|
'name': 'product',
|
|
'list_price': 0.0,
|
|
'type': 'consu',
|
|
'invoice_policy': 'order',
|
|
})
|
|
product.with_context({'fsm_task_id': task_second.id}).fsm_add_quantity()
|
|
|
|
if task_second_so.state == 'draft':
|
|
task_second_so.action_confirm() # needed if stock is not installed
|
|
self.assertRecordValues(task_second_so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'no'},
|
|
])
|
|
self.assertEqual(task_second_so.invoice_status, 'invoiced')
|
|
|
|
def test_invoice_status_so_anglo_saxon_task_non_anglo_saxon(self):
|
|
"""
|
|
When the SO's company has Anglo-Saxon accounting enabled and the
|
|
material price is zero, the invoice status must be 'to invoice'.
|
|
The task's company Anglo-Saxon setting must have no influence.
|
|
"""
|
|
self.env.company.anglo_saxon_accounting = True
|
|
self.company_data_2['company'].anglo_saxon_accounting = False
|
|
fsm_project_second = self.env['project.project'].create({
|
|
'name': 'Field Service second',
|
|
'is_fsm': True,
|
|
'company_id': self.company_data_2['company'].id,
|
|
})
|
|
task_second = self.env['project.task'].create({
|
|
'name': 'Fsm task second',
|
|
'project_id': fsm_project_second.id,
|
|
'partner_id': self.partner_1.id,
|
|
})
|
|
self.assertFalse(task_second.company_id.anglo_saxon_accounting)
|
|
task_second_so = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'project_id': fsm_project_second.id,
|
|
'task_id': task_second.id,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
task_second.sale_order_id = task_second_so
|
|
self.assertTrue(task_second_so.company_id.anglo_saxon_accounting)
|
|
|
|
product = self.env['product.product'].create({
|
|
'name': 'product',
|
|
'list_price': 0.0,
|
|
'type': 'consu',
|
|
'invoice_policy': 'order',
|
|
})
|
|
product.with_context({'fsm_task_id': task_second.id}).fsm_add_quantity()
|
|
|
|
if task_second_so.state == 'draft':
|
|
task_second_so.action_confirm() # needed if stock is not installed
|
|
self.assertRecordValues(task_second_so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'to invoice'},
|
|
])
|
|
self.assertEqual(task_second_so.invoice_status, 'to invoice')
|
|
|
|
def test_invoice_status_with_anglo_saxon(self):
|
|
"""
|
|
Verify SO and order line invoice statuses across multiple scenarios
|
|
when Anglo-Saxon accounting is enabled on the SO's company.
|
|
Material lines at price 0 added via FSM must be invoiced like any other line.
|
|
"""
|
|
self.task.partner_id = self.partner_1.id
|
|
self.env.company.anglo_saxon_accounting = True
|
|
preexisting_product = self.env['product.product'].create({
|
|
'name': 'pre existing',
|
|
'list_price': 0.0,
|
|
'type': 'consu',
|
|
'invoice_policy': 'order',
|
|
})
|
|
|
|
# Case 0: Pre-added product with price is 0 in SO are always to invoice
|
|
so = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': preexisting_product.id,
|
|
'price_unit': 0.0,
|
|
}),
|
|
],
|
|
})
|
|
# Link the task here to ensure the pre-added products aren't considered as added materials
|
|
self.task.sale_order_id = so
|
|
so.action_confirm()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'to invoice',
|
|
"SO invoice status should be 'to invoice' when anglo-saxon accounting is enabled and lines are not invoiced."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'to invoice'},
|
|
])
|
|
|
|
# Case 1: Adding a material at price 0 keeps the SO 'to invoice'; the line is 'to invoice'
|
|
self.env.company.anglo_saxon_accounting = True
|
|
self.consu_product_ordered.list_price = 0.0
|
|
self.consu_product_ordered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'to invoice',
|
|
"SO invoice status should be 'to invoice' when anglo-saxon accounting is enabled and lines are not invoiced."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'to invoice'},
|
|
{'price_unit': 0.0, 'invoice_status': 'to invoice'},
|
|
])
|
|
so._create_invoices()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'invoiced',
|
|
"SO invoice status should be 'invoiced' after invoicing the anglo-saxon line."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
])
|
|
|
|
# Case 2: Adding a priced line brings the SO back to 'to invoice'; the line is 'to invoice'
|
|
self.service_product_ordered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'to invoice',
|
|
"SO invoice status should be 'to invoice' when there is still a line to invoice."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 885.0, 'invoice_status': 'to invoice'},
|
|
])
|
|
|
|
# Case 3: After invoicing all lines, the SO is 'invoiced'
|
|
so._create_invoices()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'invoiced',
|
|
"SO invoice status should be 'invoiced' after all lines are invoiced."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 885.0, 'invoice_status': 'invoiced'},
|
|
])
|
|
|
|
def test_invoice_status_without_anglo_saxon(self):
|
|
"""
|
|
Verify SO and order line invoice statuses across multiple scenarios
|
|
when Anglo-Saxon accounting is disabled on the SO's company.
|
|
Material lines at price 0 added via FSM are ignored ('no') and must
|
|
not prevent the SO from reaching 'invoiced'.
|
|
"""
|
|
self.task.partner_id = self.partner_1.id
|
|
self.env.company.anglo_saxon_accounting = False
|
|
preexisting_product = self.env['product.product'].create({
|
|
'name': 'pre existing',
|
|
'list_price': 0.0,
|
|
'type': 'consu',
|
|
'invoice_policy': 'order',
|
|
})
|
|
|
|
# Case 0: A pre-existing line at price 0 is always 'to invoice'
|
|
so = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': preexisting_product.id,
|
|
'price_unit': 0.0,
|
|
}),
|
|
],
|
|
})
|
|
# Link the task here to ensure the pre-added products aren't considered as added materials
|
|
self.task.sale_order_id = so
|
|
so.action_confirm()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'to invoice',
|
|
"SO invoice status should be 'to invoice' when anglo-saxon accounting is enabled and lines are not invoiced."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'to invoice'},
|
|
])
|
|
so._create_invoices()
|
|
|
|
# Case 1: Adding a material at price 0 keeps the SO 'invoiced'; the line is 'no'
|
|
self.service_product_delivered.list_price = 0.0
|
|
self.service_product_delivered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'invoiced',
|
|
"SO invoice status should be 'invoiced' when all remaining lines are material lines with price 0 (non-anglo-saxon)."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 0.0, 'invoice_status': 'no'},
|
|
])
|
|
|
|
# Case 2: Adding a priced line brings the SO back to 'to invoice'; the line is 'to invoice'
|
|
self.service_product_ordered.with_user(self.project_user).with_context({'fsm_task_id': self.task.id}).fsm_add_quantity()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'to invoice',
|
|
"SO invoice status should be 'to invoice' when there is still a line to invoice."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 0.0, 'invoice_status': 'no'},
|
|
{'price_unit': 885.0, 'invoice_status': 'to invoice'},
|
|
])
|
|
|
|
# Case 3: After invoicing all lines, the SO is 'invoiced' despite the 'no' line
|
|
so._create_invoices()
|
|
self.assertEqual(
|
|
so.invoice_status,
|
|
'invoiced',
|
|
"SO invoice status should be 'invoiced' after all lines are invoiced."
|
|
)
|
|
self.assertRecordValues(so.order_line, [
|
|
{'price_unit': 0.0, 'invoice_status': 'invoiced'},
|
|
{'price_unit': 0.0, 'invoice_status': 'no'},
|
|
{'price_unit': 885.0, 'invoice_status': 'invoiced'},
|
|
])
|
|
|
|
def test_sale_order_unlink_on_sale_item_removal(self):
|
|
"""
|
|
When a sales order line is unlinked from a task, the sales order should also
|
|
be unlinked, unless the task is related to FSM.
|
|
"""
|
|
fsm_product = self.env['product.product'].create(
|
|
{
|
|
'name': "Fsm Product",
|
|
'type': 'service',
|
|
'service_policy': 'ordered_prepaid',
|
|
'project_id': self.fsm_project.id,
|
|
'service_tracking': 'task_global_project',
|
|
}
|
|
)
|
|
self.service_timesheet.service_tracking = "task_in_project"
|
|
sale_order, fsm_order = self.env['sale.order'].create([
|
|
{
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [Command.create({'product_id': self.service_timesheet.id})]
|
|
},
|
|
{
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [Command.create({'product_id': fsm_product.id})]
|
|
}
|
|
])
|
|
(sale_order | fsm_order).action_confirm()
|
|
self.assertEqual(len(sale_order.tasks_ids), 1)
|
|
self.assertEqual(len(fsm_order.tasks_ids), 1)
|
|
|
|
task = sale_order.tasks_ids
|
|
fsm_task = fsm_order.tasks_ids
|
|
(task | fsm_task).sale_line_id = False
|
|
self.assertFalse(task.sale_order_id.id)
|
|
self.assertTrue(fsm_task.sale_order_id.id)
|
|
|
|
def test_create_fsm_project_from_sale_order(self):
|
|
"""
|
|
Test: Create an FSM project from a sale order and link it to the sale order.
|
|
|
|
Steps:
|
|
1. Create an FSM project template.
|
|
2. Create a sale order with a service product.
|
|
3. Confirm the sale order.
|
|
4. Create a project from the sale order.
|
|
5. Select the fsm project template to the project.
|
|
6. Create project.
|
|
"""
|
|
|
|
fsm_project_template = self.env['project.project'].create({
|
|
'name': 'FSM Project template',
|
|
'is_template': True,
|
|
'is_fsm': True,
|
|
'company_id': self.company_data_2['company'].id,
|
|
})
|
|
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': self.product_delivery_timesheet1.id,
|
|
'product_uom_qty': 10,
|
|
}),
|
|
],
|
|
})
|
|
sale_order.action_confirm()
|
|
|
|
action = sale_order.action_create_project()
|
|
view_id = self.env.ref('sale_project.sale_project_view_form_simplified_template').id
|
|
|
|
with Form(self.env[action['res_model']].with_context(action['context']), view=view_id) as wizard:
|
|
wizard.template_id = fsm_project_template
|
|
task_action = wizard.save().action_create_project_from_so()
|
|
project = self.env['project.project'].browse(task_action['context'].get('default_project_id'))
|
|
self.assertFalse(
|
|
project.sale_order_id,
|
|
"FSM project should not be linked to any sale order."
|
|
)
|
|
self.assertFalse(
|
|
sale_order.project_id,
|
|
"Sale order should not be linked to any FSM project."
|
|
)
|
|
|
|
def test_fsm_message_post_returns_recordset(self):
|
|
"""
|
|
Ensure `message_post` returns an empty `mail.message` recordset
|
|
when `fsm_no_message_post` is set, avoiding crashes in automation
|
|
wrappers expecting a recordset.
|
|
"""
|
|
fsm_product = self.service_product_ordered
|
|
fsm_product.service_tracking = 'task_global_project'
|
|
fsm_product.project_id = self.fsm_project
|
|
|
|
sale_order = self.env['sale.order'].create({
|
|
'partner_id': self.partner_1.id,
|
|
'order_line': [
|
|
Command.create({
|
|
'product_id': fsm_product.id,
|
|
'product_uom_qty': 1,
|
|
}),
|
|
],
|
|
})
|
|
sale_order.action_confirm()
|
|
task = sale_order.tasks_ids
|
|
self.assertTrue(task)
|
|
|
|
# Verify message_post returns a mail.message recordset
|
|
# when the fsm_no_message_post context flag is set.
|
|
result = sale_order.with_context(fsm_no_message_post=True).message_post(body="test")
|
|
self.assertTrue(isinstance(result, self.env.registry['mail.message']))
|
|
self.assertFalse(result)
|
|
product_ctx = fsm_product.with_context(fsm_task_id=task.id)
|
|
product_ctx.set_fsm_quantity(5)
|
|
sol = sale_order.order_line.filtered(lambda l: l.product_id == fsm_product)
|
|
self.assertEqual(sol.product_uom_qty, 5)
|