test_project_profitability.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo.tests import tagged
  4. from .common import TestCommonSaleTimesheet
  5. @tagged('-at_install', 'post_install')
  6. class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet):
  7. @classmethod
  8. def setUpClass(cls, chart_template_ref=None):
  9. super().setUpClass(chart_template_ref=chart_template_ref)
  10. cls.task = cls.env['project.task'].create({
  11. 'name': 'Test',
  12. 'project_id': cls.project_task_rate.id,
  13. })
  14. cls.project_profitability_items_empty = {
  15. 'revenues': {'data': [], 'total': {'to_invoice': 0.0, 'invoiced': 0.0}},
  16. 'costs': {'data': [], 'total': {'to_bill': 0.0, 'billed': 0.0}},
  17. }
  18. def test_profitability_of_non_billable_project(self):
  19. """ Test no data is found for the project profitability since the project is not billable
  20. even if it is linked to a sale order items.
  21. """
  22. self.assertFalse(self.project_non_billable.allow_billable)
  23. self.assertDictEqual(
  24. self.project_non_billable._get_profitability_items(False),
  25. self.project_profitability_items_empty,
  26. )
  27. self.project_non_billable.write({'sale_line_id': self.so.order_line[0].id})
  28. self.assertDictEqual(
  29. self.project_non_billable._get_profitability_items(False),
  30. self.project_profitability_items_empty,
  31. "Even if the project has a sale order item linked the project profitability should not be computed since it is not billable."
  32. )
  33. def test_get_project_profitability_items(self):
  34. """ Test _get_project_profitability_items method to ensure the project profitability
  35. is correctly computed as expected.
  36. """
  37. sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
  38. 'partner_id': self.partner_b.id,
  39. 'partner_invoice_id': self.partner_b.id,
  40. 'partner_shipping_id': self.partner_b.id,
  41. })
  42. SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True, default_order_id=sale_order.id)
  43. delivery_service_order_line = SaleOrderLine.create({
  44. 'product_id': self.product_delivery_manual1.id,
  45. 'product_uom_qty': 5,
  46. })
  47. sale_order.action_confirm()
  48. self.task.write({'sale_line_id': delivery_service_order_line.id})
  49. self.assertDictEqual(
  50. self.project_task_rate._get_profitability_items(False),
  51. self.project_profitability_items_empty,
  52. 'No timesheets has been recorded in the task and no product has been deelivered in the SO linked so the project profitability has no data found.'
  53. )
  54. Timesheet = self.env['account.analytic.line'].with_context(
  55. default_task_id=self.task.id,
  56. )
  57. timesheet1 = Timesheet.create({
  58. 'name': 'Timesheet 1',
  59. 'employee_id': self.employee_user.id,
  60. 'project_id': self.project_task_rate.id,
  61. 'unit_amount': 3.0,
  62. })
  63. timesheet2 = Timesheet.create({
  64. 'name': 'Timesheet 2',
  65. 'employee_id': self.employee_user.id,
  66. 'project_id': self.project_task_rate.id,
  67. 'unit_amount': 2.0,
  68. })
  69. sequence_per_invoice_type = self.project_task_rate._get_profitability_sequence_per_invoice_type()
  70. self.assertIn('billable_time', sequence_per_invoice_type)
  71. self.assertIn('billable_fixed', sequence_per_invoice_type)
  72. self.assertIn('billable_milestones', sequence_per_invoice_type)
  73. self.assertIn('billable_manual', sequence_per_invoice_type)
  74. self.assertEqual(self.task.sale_line_id, delivery_service_order_line)
  75. self.assertEqual((timesheet1 + timesheet2).so_line, delivery_service_order_line)
  76. self.assertEqual(delivery_service_order_line.qty_delivered, 0.0, 'The service type is not timesheet but manual so the quantity delivered is not increased by the timesheets linked.')
  77. self.assertDictEqual(
  78. self.project_task_rate._get_profitability_items(False),
  79. {
  80. 'revenues': {
  81. 'data': [],
  82. 'total': {'to_invoice': 0.0, 'invoiced': 0.0},
  83. },
  84. 'costs': {
  85. 'data': [
  86. {
  87. 'id': 'billable_manual',
  88. 'sequence': sequence_per_invoice_type['billable_manual'],
  89. 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost,
  90. 'to_bill': 0.0,
  91. },
  92. ],
  93. 'total': {
  94. 'to_bill': 0.0,
  95. 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost
  96. },
  97. },
  98. }
  99. )
  100. timesheet3 = Timesheet.create({
  101. 'name': 'Timesheet 3',
  102. 'employee_id': self.employee_manager.id,
  103. 'project_id': self.project_task_rate.id,
  104. 'unit_amount': 1.0,
  105. 'so_line': False,
  106. 'is_so_line_edited': True,
  107. })
  108. self.assertFalse(timesheet3.so_line, 'This timesheet should be non billable since the user manually empty the SOL.')
  109. self.assertDictEqual(
  110. self.project_task_rate._get_profitability_items(False),
  111. {
  112. 'revenues': {
  113. 'data': [],
  114. 'total': {'to_invoice': 0.0, 'invoiced': 0.0},
  115. },
  116. 'costs': {
  117. 'data': [
  118. {
  119. 'id': 'billable_manual',
  120. 'sequence': sequence_per_invoice_type['billable_manual'],
  121. 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost,
  122. 'to_bill': 0.0,
  123. },
  124. {
  125. 'id': 'non_billable',
  126. 'sequence': sequence_per_invoice_type['non_billable'],
  127. 'billed': timesheet3.unit_amount * -self.employee_manager.hourly_cost,
  128. 'to_bill': 0.0,
  129. },
  130. ],
  131. 'total': {
  132. 'to_bill': 0.0,
  133. 'billed':
  134. (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost
  135. + timesheet3.unit_amount * -self.employee_manager.hourly_cost,
  136. },
  137. },
  138. },
  139. 'The previous costs should remains and the cost of the third timesheet should be added.'
  140. )
  141. delivery_timesheet_order_line = SaleOrderLine.create({
  142. 'product_id': self.product_delivery_timesheet1.id,
  143. 'product_uom_qty': 5,
  144. })
  145. self.task.write({'sale_line_id': delivery_timesheet_order_line.id})
  146. billable_timesheets = timesheet1 + timesheet2
  147. self.assertEqual(billable_timesheets.so_line, delivery_timesheet_order_line, 'The SOL of the timesheets should be the one of the task.')
  148. self.assertEqual(delivery_timesheet_order_line.qty_delivered, timesheet1.unit_amount + timesheet2.unit_amount)
  149. self.assertEqual(
  150. self.project_task_rate._get_profitability_items(False),
  151. {
  152. 'revenues': {
  153. 'data': [
  154. {'id': 'billable_time', 'sequence': sequence_per_invoice_type['billable_time'], 'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
  155. ],
  156. 'total': {'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
  157. },
  158. 'costs': {
  159. 'data': [
  160. {
  161. 'id': 'billable_time',
  162. 'sequence': sequence_per_invoice_type['billable_time'],
  163. 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost,
  164. 'to_bill': 0.0,
  165. },
  166. {
  167. 'id': 'non_billable',
  168. 'sequence': sequence_per_invoice_type['non_billable'],
  169. 'billed': timesheet3.unit_amount * -self.employee_manager.hourly_cost,
  170. 'to_bill': 0.0,
  171. },
  172. ],
  173. 'total': {
  174. 'to_bill': 0.0,
  175. 'billed':
  176. (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost
  177. + timesheet3.unit_amount * -self.employee_manager.hourly_cost,
  178. },
  179. },
  180. },
  181. )
  182. milestone_order_line = SaleOrderLine.create({
  183. 'product_id': self.product_milestone.id,
  184. 'product_uom_qty': 1,
  185. })
  186. task2 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
  187. 'name': 'Test',
  188. 'project_id': self.project_task_rate.id,
  189. 'sale_line_id': milestone_order_line.id,
  190. })
  191. task2_timesheet = Timesheet.with_context(default_task_id=task2.id).create({
  192. 'name': '/',
  193. 'project_id': self.project_task_rate.id,
  194. 'employee_id': self.employee_user.id,
  195. 'unit_amount': 1,
  196. })
  197. self.assertEqual(task2_timesheet.so_line, milestone_order_line)
  198. profitability_items = self.project_task_rate._get_profitability_items(False)
  199. self.assertFalse([data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'])
  200. self.assertDictEqual(
  201. [data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'][0],
  202. {'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_bill': 0.0, 'billed': task2_timesheet.amount},
  203. )
  204. milestone_order_line.qty_delivered = 1
  205. profitability_items = self.project_task_rate._get_profitability_items(False)
  206. self.assertDictEqual(
  207. [data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'][0],
  208. {'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_invoice': milestone_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
  209. )
  210. task2_timesheet.unlink()
  211. profitability_items = self.project_task_rate._get_profitability_items(False)
  212. self.assertFalse([data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'])
  213. self.assertFalse([data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'])