Skip to content

Commit e5a251f

Browse files
committed
[18.0][ADD] project_revenue_recognition
1 parent 56fbb1e commit e5a251f

21 files changed

Lines changed: 1072 additions & 0 deletions
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
===========================
2+
Project Revenue Recognition
3+
===========================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:0e6273bfc723c70d6d40acdaeb0b898fdaf2d3b6bcac5b2a2ab0a5519a1281d2
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproject-lightgray.png?logo=github
20+
:target: https://github.com/OCA/project/tree/18.0/project_revenue_recognition
21+
:alt: OCA/project
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/project-18-0/project-18-0-project_revenue_recognition
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/project&target_branch=18.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module extends the functionality of the project module to allow
32+
revenue recognition.
33+
34+
**Table of contents**
35+
36+
.. contents::
37+
:local:
38+
39+
Usage
40+
=====
41+
42+
43+
44+
Bug Tracker
45+
===========
46+
47+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/project/issues>`_.
48+
In case of trouble, please check there if your issue has already been reported.
49+
If you spotted it first, help us to smash it by providing a detailed and welcomed
50+
`feedback <https://github.com/OCA/project/issues/new?body=module:%20project_revenue_recognition%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
51+
52+
Do not contact contributors directly about support or help with technical issues.
53+
54+
Credits
55+
=======
56+
57+
Authors
58+
-------
59+
60+
* Ecosoft
61+
62+
Contributors
63+
------------
64+
65+
- `Ecosoft <https://ecosoft.co.th>`__:
66+
67+
- Saran Lim. saranl@ecosoft.co.th
68+
69+
Maintainers
70+
-----------
71+
72+
This module is maintained by the OCA.
73+
74+
.. image:: https://odoo-community.org/logo.png
75+
:alt: Odoo Community Association
76+
:target: https://odoo-community.org
77+
78+
OCA, or the Odoo Community Association, is a nonprofit organization whose
79+
mission is to support the collaborative development of Odoo features and
80+
promote its widespread use.
81+
82+
.. |maintainer-Saran440| image:: https://github.com/Saran440.png?size=40px
83+
:target: https://github.com/Saran440
84+
:alt: Saran440
85+
86+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
87+
88+
|maintainer-Saran440|
89+
90+
This module is part of the `OCA/project <https://github.com/OCA/project/tree/18.0/project_revenue_recognition>`_ project on GitHub.
91+
92+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
2+
3+
from . import models
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2026 Ecosoft Co., Ltd (https://ecosoft.co.th/)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
3+
4+
{
5+
"name": "Project Revenue Recognition",
6+
"summary": "Add Revenue Recognition on Project",
7+
"version": "18.0.1.0.0",
8+
"license": "AGPL-3",
9+
"author": "Ecosoft, Odoo Community Association (OCA)",
10+
"website": "https://github.com/OCA/project",
11+
"depends": ["account", "sale_project"],
12+
"data": [
13+
"data/sequence.xml",
14+
"security/ir.model.access.csv",
15+
"security/project_revenue_recognition.xml",
16+
"views/res_config_settings_views.xml",
17+
"views/project_revenue_recognition_view.xml",
18+
"views/account_move_views.xml",
19+
],
20+
"maintainers": ["Saran440"],
21+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<odoo noupdate="1">
3+
<record id="project_revenue_recognition_code_seq" model="ir.sequence">
4+
<field name="name">Project Revenue Recognition</field>
5+
<field name="code">project.revenue.recognition</field>
6+
<field name="prefix">PRR</field>
7+
<field name="padding">5</field>
8+
<field name="company_id" eval="False" />
9+
</record>
10+
</odoo>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
2+
3+
from . import res_company
4+
from . import res_config_settings
5+
from . import project_project
6+
from . import project_revenue_recognition
7+
from . import account_move
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright 2026 Ecosoft Co., Ltd (https://ecosoft.co.th)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import fields, models
5+
6+
7+
class AccountMove(models.Model):
8+
_inherit = "account.move"
9+
10+
project_revenue_recognition_id = fields.Many2one(
11+
comodel_name="project.revenue.recognition",
12+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright 2026 Ecosoft Co., Ltd (https://ecosoft.co.th)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import models
5+
6+
7+
class ProjectProject(models.Model):
8+
_inherit = "project.project"
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Copyright 2026 Ecosoft Co., Ltd (https://ecosoft.co.th)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from odoo import Command, api, fields, models
5+
6+
7+
class ProjectRevenueRecognition(models.Model):
8+
_name = "project.revenue.recognition"
9+
_inherit = ["mail.thread", "mail.activity.mixin"]
10+
_description = "Project Revenue Recognition"
11+
_order = "name desc"
12+
13+
name = fields.Char(default="/", required=True, copy=False)
14+
project_id = fields.Many2one(
15+
comodel_name="project.project",
16+
required=True,
17+
ondelete="cascade",
18+
)
19+
date = fields.Date(
20+
default=fields.Date.today,
21+
required=True,
22+
string="Accounting Date",
23+
)
24+
company_id = fields.Many2one(
25+
comodel_name="res.company",
26+
required=True,
27+
default=lambda self: self.env.company,
28+
)
29+
currency_id = fields.Many2one(
30+
comodel_name="res.currency",
31+
related="company_id.currency_id",
32+
)
33+
journal_id = fields.Many2one(
34+
comodel_name="account.journal",
35+
default=lambda self: self.env.company.project_revenue_recognition_journal_id,
36+
required=True,
37+
)
38+
move_ids = fields.One2many(
39+
comodel_name="account.move",
40+
inverse_name="project_revenue_recognition_id",
41+
)
42+
move_count = fields.Integer(
43+
compute="_compute_move_count",
44+
)
45+
project_percentage = fields.Float(
46+
compute="_compute_project_percentage",
47+
store=True,
48+
)
49+
project_amount = fields.Monetary(
50+
compute="_compute_project_amount",
51+
store=True,
52+
)
53+
invoice_percentage = fields.Float(
54+
compute="_compute_invoice_percentage",
55+
store=True,
56+
)
57+
invoice_amount = fields.Monetary(
58+
compute="_compute_invoice_amount",
59+
store=True,
60+
)
61+
diff_amount = fields.Monetary(
62+
compute="_compute_diff_amount",
63+
store=True,
64+
)
65+
state = fields.Selection(
66+
selection=[
67+
("draft", "Draft"),
68+
("confirm", "Confirm"),
69+
("done", "Done"),
70+
("reverse", "Reverse"),
71+
("cancel", "Cancel"),
72+
],
73+
default="draft",
74+
)
75+
76+
def _get_amount_sale_invoice(self):
77+
all_sale_orders = (
78+
self.project_id._fetch_sale_order_items(
79+
{"project.task": [("is_closed", "=", False)]}
80+
)
81+
.sudo()
82+
.order_id
83+
)
84+
so_amount_total = sum(all_sale_orders.mapped("amount_total"))
85+
so_invoices = all_sale_orders.mapped("invoice_ids").filtered(
86+
lambda inv: inv.state == "posted"
87+
)
88+
inv_amount_total = sum(so_invoices.mapped("amount_total"))
89+
return inv_amount_total, so_amount_total
90+
91+
@api.depends("project_id.task_completion_percentage")
92+
def _compute_project_percentage(self):
93+
for rec in self:
94+
rec.project_percentage = rec.project_id.task_completion_percentage
95+
96+
@api.depends("project_percentage")
97+
def _compute_project_amount(self):
98+
for rec in self:
99+
rec.project_amount = (
100+
rec.project_percentage * rec._get_amount_sale_invoice()[1]
101+
)
102+
103+
@api.depends("project_id")
104+
def _compute_invoice_percentage(self):
105+
for rec in self:
106+
inv_amount_total, so_amount_total = rec._get_amount_sale_invoice()
107+
if so_amount_total:
108+
rec.invoice_percentage = inv_amount_total / so_amount_total
109+
110+
@api.depends("project_id")
111+
def _compute_invoice_amount(self):
112+
for rec in self:
113+
inv_amount_total, so_amount_total = rec._get_amount_sale_invoice()
114+
rec.invoice_amount = inv_amount_total
115+
116+
@api.depends("project_amount", "invoice_amount")
117+
def _compute_diff_amount(self):
118+
for rec in self:
119+
rec.diff_amount = rec.project_amount - rec.invoice_amount
120+
121+
@api.depends("move_ids")
122+
def _compute_move_count(self):
123+
for rec in self:
124+
rec.move_count = len(rec.move_ids)
125+
126+
def _get_move_dict(self):
127+
self.ensure_one()
128+
return {
129+
"move_type": "entry",
130+
"journal_id": self.journal_id.id,
131+
"date": self.date,
132+
"ref": self.name,
133+
"project_revenue_recognition_id": self.id,
134+
}
135+
136+
def _get_move_line_dict(self):
137+
self.ensure_one()
138+
company = self.company_id
139+
return [
140+
Command.create(
141+
{
142+
"account_id": company.project_revenue_account_id.id,
143+
"name": self.name,
144+
"debit": self.project_amount if self.diff_amount > 0 else 0,
145+
"credit": self.project_amount if self.diff_amount < 0 else 0,
146+
}
147+
),
148+
Command.create(
149+
{
150+
"account_id": company.project_expense_account_id.id,
151+
"name": self.name,
152+
"debit": self.project_amount if self.diff_amount < 0 else 0,
153+
"credit": self.project_amount if self.diff_amount > 0 else 0,
154+
}
155+
),
156+
]
157+
158+
def _create_moves(self):
159+
move_dict = []
160+
for rec in self:
161+
move = rec._get_move_dict()
162+
move["line_ids"] = rec._get_move_line_dict()
163+
move_dict.append(move)
164+
moves = self.env["account.move"].create(move_dict)
165+
return moves
166+
167+
@api.model_create_multi
168+
def create(self, vals_list):
169+
for vals in vals_list:
170+
if vals.get("name", "/") == "/":
171+
name = (
172+
self.env["ir.sequence"].next_by_code("project.revenue.recognition")
173+
or "/"
174+
)
175+
vals["name"] = name
176+
return super().create(vals_list)
177+
178+
def action_confirm(self):
179+
return self.write({"state": "confirm"})
180+
181+
def action_adjust_cost(self):
182+
self._create_moves()
183+
return self.write({"state": "done"})
184+
185+
def action_reverse_cost(self):
186+
# NOTE: Reverse cost is not implemented
187+
return self.write({"state": "reverse"})
188+
189+
def action_cancel(self):
190+
# NOTE: Cancel is not implemented
191+
return self.write({"state": "cancel"})
192+
193+
def action_draft(self):
194+
return self.write({"state": "draft"})
195+
196+
def action_view_move(self):
197+
self.ensure_one()
198+
action = {
199+
"name": self.env._("Journal Entries"),
200+
"view_mode": "list,form",
201+
"res_model": "account.move",
202+
"type": "ir.actions.act_window",
203+
"domain": [("id", "in", self.move_ids.ids)],
204+
"views": [
205+
[self.env.ref("account.view_move_tree").id, "list"],
206+
[self.env.ref("account.view_move_form").id, "form"],
207+
],
208+
}
209+
return action
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from odoo import fields, models
5+
6+
7+
class ResCompany(models.Model):
8+
_inherit = "res.company"
9+
10+
project_revenue_account_id = fields.Many2one(
11+
comodel_name="account.account",
12+
string="Revenue Account",
13+
)
14+
project_expense_account_id = fields.Many2one(
15+
comodel_name="account.account",
16+
string="Expense Account",
17+
)
18+
project_revenue_recognition_journal_id = fields.Many2one(
19+
comodel_name="account.journal",
20+
string="Revenue Recognition Journal",
21+
)

0 commit comments

Comments
 (0)