From 3e55ff200b23ba016c8ac37b20993d4aa31d225c Mon Sep 17 00:00:00 2001 From: "Pranjali Sangavekar(prsan)" Date: Wed, 3 Jun 2026 17:36:56 +0530 Subject: [PATCH] [ADD] revenue_recognition: link project start date to journal entry recognition date When a project start date changes, the recognition date on related journal items was not updated automatically. This required manual intervention through invoices, making revenue recognition error-prone and time-consuming. - Add button on project form to trigger recognition date update. - Add non-blocking warning when planned date differs from the recognition date on related journal items. - Add wizard to preview and create adjusting journal entries based on the updated project start date. --- revenue_recognition/__init__.py | 2 + revenue_recognition/__manifest__.py | 15 +++ revenue_recognition/models/__init__.py | 2 + .../models/account_automatic_entry_wizard.py | 39 +++++++ revenue_recognition/models/project_project.py | 109 ++++++++++++++++++ .../project_revenue_recognition_views.xml | 47 ++++++++ .../account_automatic_entry_wizard_views.xml | 7 ++ 7 files changed, 221 insertions(+) create mode 100644 revenue_recognition/__init__.py create mode 100644 revenue_recognition/__manifest__.py create mode 100644 revenue_recognition/models/__init__.py create mode 100644 revenue_recognition/models/account_automatic_entry_wizard.py create mode 100644 revenue_recognition/models/project_project.py create mode 100644 revenue_recognition/views/project_revenue_recognition_views.xml create mode 100644 revenue_recognition/wizard/account_automatic_entry_wizard_views.xml diff --git a/revenue_recognition/__init__.py b/revenue_recognition/__init__.py new file mode 100644 index 00000000000..cf3a78e262c --- /dev/null +++ b/revenue_recognition/__init__.py @@ -0,0 +1,2 @@ +# Revenue Recognition Management Module +from . import models diff --git a/revenue_recognition/__manifest__.py b/revenue_recognition/__manifest__.py new file mode 100644 index 00000000000..ece63ff9a0d --- /dev/null +++ b/revenue_recognition/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': 'Revenue Recognition Management', + 'version': '1.0', + 'category': 'Accounting', + 'summary': 'Automatic revenue recognition for projects with date correction', + 'author': 'Odoo', + 'depends': ['project', 'sale', 'account', 'sale_project'], + 'data': [ + 'views/project_revenue_recognition_views.xml', + 'wizard/account_automatic_entry_wizard_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/revenue_recognition/models/__init__.py b/revenue_recognition/models/__init__.py new file mode 100644 index 00000000000..ac54e1bd4a6 --- /dev/null +++ b/revenue_recognition/models/__init__.py @@ -0,0 +1,2 @@ +from . import project_project +from . import account_automatic_entry_wizard diff --git a/revenue_recognition/models/account_automatic_entry_wizard.py b/revenue_recognition/models/account_automatic_entry_wizard.py new file mode 100644 index 00000000000..3a09ad0be38 --- /dev/null +++ b/revenue_recognition/models/account_automatic_entry_wizard.py @@ -0,0 +1,39 @@ +from odoo import models, fields + + +class AccountAutomaticEntryWizard(models.TransientModel): + _inherit = 'account.automatic.entry.wizard' + + date = fields.Date( + required=True, + default=lambda self: self._get_default_date(), + readonly=False + ) + + def _get_default_date(self): + project_id = ( + self.env.context.get('project_id') or self.env.context.get('create_for_project_id') + ) + + if project_id: + project = self.env['project.project'].browse(project_id) + if project.exists() and project.date_start: + return project.date_start + + if self.env.context.get('active_model') == 'account.move.line': + for line in self.env['account.move.line'].browse( + self.env.context.get('active_ids', []) + ): + if line.analytic_distribution: + for analytic_id_str in line.analytic_distribution: + try: + project = self.env['project.project'].search( + [('account_id', '=', int(analytic_id_str))], + limit=1 + ) + if project and project.date_start: + return project.date_start + except (ValueError, TypeError): + pass + + return fields.Date.context_today(self) diff --git a/revenue_recognition/models/project_project.py b/revenue_recognition/models/project_project.py new file mode 100644 index 00000000000..c825ac85a38 --- /dev/null +++ b/revenue_recognition/models/project_project.py @@ -0,0 +1,109 @@ +from odoo import models, fields, api + + +class ProjectProject(models.Model): + _inherit = 'project.project' + + has_unrecognized_entries = fields.Boolean( + string='Has Unrecognized Entries', + compute='_compute_has_unrecognized_entries', + store=False, + ) + + unrecognized_entries_message = fields.Char( + string='Unrecognized Entries Message', + compute='_compute_unrecognized_entries_message', + ) + + @api.depends('date_start', 'sale_order_id') + def _compute_has_unrecognized_entries(self): + for project in self: + project.has_unrecognized_entries = False + + if not project.sale_order_id or not project.date_start: + continue + + invoices = project.sale_order_id.invoice_ids + + if not invoices: + continue + + generated_entries = self.env['account.move'].search([ + ('adjusting_entry_origin_move_ids', 'in', invoices.ids), + ('state', '=', 'posted') + ]) + + has_current_recognition = generated_entries.filtered( + lambda m: m.date == project.date_start + ) + project.has_unrecognized_entries = not bool(has_current_recognition) + + @api.depends('has_unrecognized_entries', 'date_start') + def _compute_unrecognized_entries_message(self): + for project in self: + if project.has_unrecognized_entries and project.date_start: + project.unrecognized_entries_message = ( + f"You still have journal items that need to be recognised " + f"from {project.date_start.strftime('%m/%d/%Y')}" + ) + else: + project.unrecognized_entries_message = False + + def _get_original_invoice_lines(self, move_lines): + generated_entries = move_lines.filtered( + lambda l: bool(l.move_id.adjusting_entry_origin_move_ids) + ) + + if not generated_entries: + return move_lines.filtered( + lambda l: l.account_id.account_type == 'income' + ) + + origin_moves = generated_entries.mapped( + 'move_id.adjusting_entry_origin_move_ids' + ) + + invoice_lines = self.env['account.move.line'].search([ + ('move_id', 'in', origin_moves.ids), + ('parent_state', '=', 'posted'), + ]) + + return invoice_lines.filtered( + lambda l: l.account_id.account_type == 'income' + ) + + def action_recognize_invoices(self): + self.ensure_one() + + if not self.account_id: + return False + + move_lines = self.env['account.move.line'].search([('parent_state', '=', 'posted')]).filtered(lambda l: l.analytic_distribution and str(self.account_id.id) in l.analytic_distribution) + + original_invoice_lines = self._get_original_invoice_lines(move_lines) + + if not original_invoice_lines: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Journal Items', + 'message': 'No journal items found for this project.', + 'type': 'warning', + 'sticky': False, + } + } + + return { + 'name': 'Create Automatic Entries', + 'type': 'ir.actions.act_window', + 'res_model': 'account.automatic.entry.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'active_model': 'account.move.line', + 'active_ids': original_invoice_lines.ids, + 'project_id': self.id, + 'default_action': 'change_period', + }, + } diff --git a/revenue_recognition/views/project_revenue_recognition_views.xml b/revenue_recognition/views/project_revenue_recognition_views.xml new file mode 100644 index 00000000000..54aacefb79b --- /dev/null +++ b/revenue_recognition/views/project_revenue_recognition_views.xml @@ -0,0 +1,47 @@ + + + + + + project.project.form.revenue.recognition + project.project + + + + + + + + + + + + + + + +