diff --git a/budget/__init__.py b/budget/__init__.py new file mode 100644 index 00000000000..c536983e2b2 --- /dev/null +++ b/budget/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard \ No newline at end of file diff --git a/budget/__manifest__.py b/budget/__manifest__.py new file mode 100644 index 00000000000..a476c8404f2 --- /dev/null +++ b/budget/__manifest__.py @@ -0,0 +1,15 @@ +{ + "name": "Budget", + "version": "1.0", + "depends": ["base","account"], + "data": [ + "security/ir.model.access.csv", + "wizard/budget_wizard_views.xml", + "views/budget_views.xml", + "views/budget_line_views.xml", + "views/budget_menu_views.xml", + ], + "installable": True, + "application": True, + "license": "LGPL-3", +} \ No newline at end of file diff --git a/budget/models/__init__.py b/budget/models/__init__.py new file mode 100644 index 00000000000..5e24362f59d --- /dev/null +++ b/budget/models/__init__.py @@ -0,0 +1,3 @@ +from . import budget +from . import budget_lines +from . import res_account_analytic_line \ No newline at end of file diff --git a/budget/models/budget.py b/budget/models/budget.py new file mode 100644 index 00000000000..e6ea4eab1e1 --- /dev/null +++ b/budget/models/budget.py @@ -0,0 +1,104 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError,UserError + +class Budget(models.Model): + _name = "budget.budget" + _description = "Budget" + _inherit = ['mail.thread'] + + name = fields.Char('Budget Name', compute='_compute_budget_name', store=True) + + color= fields.Integer(default=5) + user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user) + date_from = fields.Date('Start Date') + date_to = fields.Date('End Date') + active= fields.Boolean(default=True) + revision_id = fields.Many2one('budget.budget', string="Revision_id", readonly=True , tracking=True) + is_favorite = fields.Boolean(default=False) + state = fields.Selection(selection=[ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('revised', 'Revised'), + ('done', 'Done'), + ], string='Status', default='draft', required=True, readonly=True, copy=False) + + + on_over_budget = fields.Selection([ + ('warning', 'Show a warning'), + ('restriction', 'Restrict on creation'), + ], string='Over Budget Policy',default="restriction") + currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.company.currency_id) + budget_lines = fields.One2many('budget.budget.lines', 'budget_id', 'Budget Lines', copy=True) + company_id = fields.Many2one('res.company', 'Company', required=True, default=lambda self: self.env.company) + analytic_account_ids = fields.Many2many('account.analytic.account') + warnings = fields.Text(compute="_check_over_budget") + + + @api.constrains('date_from', 'date_to') + def _check_dates(self): + for record in self: + if record.date_from == record.date_to: + raise ValidationError("Start Date and End Date cannot be the same.") + elif record.date_to < record.date_from: + raise ValidationError("End Date must be after Start Date.") + else: + if record.state in ['draft', 'confirmed']: + overlapping_budgets = self.search([ + ('id', '!=', record.id), # Exclude the current record + ('state', 'in', ['draft', 'confirmed']), + ('date_from', '<=', record.date_to), + ('date_to', '>=', record.date_from), + ]) + if overlapping_budgets: + raise UserError("Cannot create or update this budget because it overlaps with another budget") + @api.depends("budget_lines.over_budget") + def _check_over_budget(self): + for record in self: + if ( + record.on_over_budget == "warning" + and any(ob > 0 for ob in record.budget_lines.mapped("over_budget")) > 0 + ): + record.warnings = "Achieved amount exceeds the budget!" + else: + record.warnings = False + + @api.depends('date_from', 'date_to') + def _compute_budget_name(self): + for record in self: + record.name = f"Budget:({record.date_from} to {record.date_to})" + + def action_budget_confirm(self): + if self.state != 'draft': + raise ValidationError("Only budgets in draft state can be confirmed.") + self.write({'state': 'confirmed'}) + + + def action_budget_revise(self): + if self.state != "confirmed": + raise UserError("Only confirmed budgets can be revised.") + self.ensure_one() + new_budget_vals = self.copy_data()[0] + new_budget_vals['revision_id'] = self.id + new_budget_vals['state'] = 'draft' + + self.active = False + self.state = 'revised' + new_budget = self.create(new_budget_vals) + self.message_post( + body=f"The budget has been revised. A new draft budget has been created with ID {new_budget.id}.", + subtype_xmlid="mail.mt_note" + ) + + return new_budget + + + def action_budget_draft(self): + if self.state not in ['confirmed', 'revised']: + raise ValidationError("Only confirmed or revised budgets can be set back to draft.") + self.write({'state': 'draft'}) + + + def action_budget_done(self): + if self.state != 'revised': + raise ValidationError("Only revised budgets can be marked as done.") + self.write({'state': 'done'}) diff --git a/budget/models/budget_lines.py b/budget/models/budget_lines.py new file mode 100644 index 00000000000..5ba85da34a5 --- /dev/null +++ b/budget/models/budget_lines.py @@ -0,0 +1,68 @@ +from odoo import fields, models,api +from odoo.exceptions import ValidationError,UserError + +class BudgetLines(models.Model): + _name = "budget.budget.lines" + _description = "Budget Line" + user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user) + budget_start_date = fields.Date(related="budget_id.date_from", readonly=True) + budget_end_date = fields.Date(related="budget_id.date_to", readonly=True) + + budget_id = fields.Many2one('budget.budget', 'Budget', ondelete='cascade', index=True, required=True) + state = fields.Selection(related="budget_id.state", readonly=True) + analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account') + currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.company.currency_id) + planned_amount = fields.Monetary( + 'Budget Amount', required=True, + default=0.0, + help="Amount you plan to earn/spend. Record a positive amount if it is a revenue and a negative amount if it is a cost.") + practical_amount = fields.Monetary(string='Achieved Amount' , default=0.0, store=True) + + over_budget = fields.Monetary( + string="Over Budget", + default=0.0, + compute="_compute_practical_amount", + help="The amount by which the budget line exceeds its allocated budget.", + store=True + ) + count= fields.Integer('Count',computed="_compute_practical_amount",default=0, readonly=True) + percentage = fields.Float(default=0.0,compute="_compute_achieved_percentage", + help="Comparison between practical and planned amount. This measure tells you if you are below or over budget.") + + + @api.depends("practical_amount", "planned_amount") + def _compute_achieved_percentage(self): + for record in self: + if record.planned_amount: + record.percentage = ( + record.practical_amount / record.planned_amount + ) * 100 + + def action_view_analytic_lines(self): + if not self.budget_id: + raise UserError("No budget linked to this budget line.") + + budget_start_date = self.budget_id.date_from + budget_end_date = self.budget_id.date_to + + return { + "type": "ir.actions.act_window", + "name": "Analytic Lines", + "res_model": "account.analytic.line", + "view_mode": "list", + "target": "current", + "context": { + "default_account_id": self.analytic_account_id.id, + "budget_start_date": budget_start_date, + "budget_end_date": budget_end_date, + }, + "domain": [ + ("account_id", "=", self.analytic_account_id.id), + ("date", ">=", budget_start_date), + ("date", "<=", budget_end_date), + ("amount", "<", 0), + ], + } + + + \ No newline at end of file diff --git a/budget/models/res_account_analytic_line.py b/budget/models/res_account_analytic_line.py new file mode 100644 index 00000000000..3d68750c223 --- /dev/null +++ b/budget/models/res_account_analytic_line.py @@ -0,0 +1,11 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + budget_line_id = fields.Many2one('budget.budget.lines', 'Budget Line') + # account_id = fields.Many2one('account.analytic.account', 'Automatic Account', related='budget_line_id.analytic_account_id.auto_account_id') + + + diff --git a/budget/security/ir.model.access.csv b/budget/security/ir.model.access.csv new file mode 100644 index 00000000000..b968629c318 --- /dev/null +++ b/budget/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +budget.access_budget_budget,access_budget_budget,budget.model_budget_budget,base.group_user,1,1,1,1 +access_budget_lines_user,access_budget_lines,model_budget_budget_lines,base.group_user,1,1,1,1 +budget.access_budget_wizard,access_budget_wizard,budget.model_budget_wizard,base.group_user,1,1,1,1 diff --git a/budget/views/budget_line_views.xml b/budget/views/budget_line_views.xml new file mode 100644 index 00000000000..c5c20e9fc1f --- /dev/null +++ b/budget/views/budget_line_views.xml @@ -0,0 +1,65 @@ + + + + + budget.line.list + budget.budget.lines + + + + + + + + + + + + + + + budget.budget.line.pivot + budget.budget.lines + + + + + + + + + + budget.budget.line.graph + budget.budget.lines + + + + + + + + + + + budget.budget.line.gantt + budget.budget.lines + + + + + + + + + + + + Budget lines + budget.budget.lines + list,pivot,graph,gantt + + + + + + \ No newline at end of file diff --git a/budget/views/budget_menu_views.xml b/budget/views/budget_menu_views.xml new file mode 100644 index 00000000000..c267473bfd6 --- /dev/null +++ b/budget/views/budget_menu_views.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/budget/views/budget_views.xml b/budget/views/budget_views.xml new file mode 100644 index 00000000000..75c63c4f92f --- /dev/null +++ b/budget/views/budget_views.xml @@ -0,0 +1,150 @@ + + + + + Budgets + budget.budget + kanban,form,list + + + + Create Multiple Budgets + budget.wizard + form + + new + + + + + + budget.budget.kanban + budget.budget + + +
+
+ + + +
+
+ +
+
+ +
+
+
+ +
+
+ +

+ +

+
+
+
+ + + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+ + + budget.budget.form + budget.budget + +
+ + +
+
+ +
Budget name
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ +

+ + + + + + + + + + + + +
+ +
+
+ + estate.property.type.list + estate.property.type + + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..963b901fe42 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,150 @@ + + + + Properties + estate.property + kanban,list,form + +
+

No properties available. Start by creating one!

+
+
+ +
+ + estate.property.kanban + estate.property + + + + +
+ + + + +
Expected Price: +
+
Best Price: +
+
Selling Price: +
+ +
+ +
+
+
+
+
+ + + estate.property.list + estate.property + + + +
+
+ + + + + + + + + + + +
+
+
+ + estate.property.form + estate.property + +
+
+
+ +

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +