From a11795f9dade4110cfbc588932a00df6b66604b6 Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Sun, 30 Aug 2026 00:48:25 -0400 Subject: [PATCH] Add Metatroncube operating system addon --- addons/mcs_operating_system/__init__.py | 1 + addons/mcs_operating_system/__manifest__.py | 35 ++ addons/mcs_operating_system/data/crm_data.xml | 62 ++++ .../mcs_operating_system/data/cron_data.xml | 11 + .../data/operating_data.xml | 27 ++ .../data/project_data.xml | 117 +++++++ .../mcs_operating_system/models/__init__.py | 7 + .../mcs_operating_system/models/crm_lead.py | 110 ++++++ .../mcs_operating_system/models/dashboard.py | 174 ++++++++++ .../models/hr_employee.py | 20 ++ .../mcs_operating_system/models/operating.py | 235 +++++++++++++ addons/mcs_operating_system/models/project.py | 146 ++++++++ .../mcs_operating_system/models/sale_order.py | 12 + .../mcs_operating_system/models/timesheet.py | 19 ++ .../security/ir.model.access.csv | 12 + .../security/security.xml | 12 + .../views/crm_lead_views.xml | 59 ++++ .../views/dashboard_views.xml | 67 ++++ .../views/hr_employee_views.xml | 13 + addons/mcs_operating_system/views/menu.xml | 16 + .../views/operating_views.xml | 314 ++++++++++++++++++ .../views/project_views.xml | 103 ++++++ .../views/timesheet_views.xml | 34 ++ 23 files changed, 1606 insertions(+) create mode 100644 addons/mcs_operating_system/__init__.py create mode 100644 addons/mcs_operating_system/__manifest__.py create mode 100644 addons/mcs_operating_system/data/crm_data.xml create mode 100644 addons/mcs_operating_system/data/cron_data.xml create mode 100644 addons/mcs_operating_system/data/operating_data.xml create mode 100644 addons/mcs_operating_system/data/project_data.xml create mode 100644 addons/mcs_operating_system/models/__init__.py create mode 100644 addons/mcs_operating_system/models/crm_lead.py create mode 100644 addons/mcs_operating_system/models/dashboard.py create mode 100644 addons/mcs_operating_system/models/hr_employee.py create mode 100644 addons/mcs_operating_system/models/operating.py create mode 100644 addons/mcs_operating_system/models/project.py create mode 100644 addons/mcs_operating_system/models/sale_order.py create mode 100644 addons/mcs_operating_system/models/timesheet.py create mode 100644 addons/mcs_operating_system/security/ir.model.access.csv create mode 100644 addons/mcs_operating_system/security/security.xml create mode 100644 addons/mcs_operating_system/views/crm_lead_views.xml create mode 100644 addons/mcs_operating_system/views/dashboard_views.xml create mode 100644 addons/mcs_operating_system/views/hr_employee_views.xml create mode 100644 addons/mcs_operating_system/views/menu.xml create mode 100644 addons/mcs_operating_system/views/operating_views.xml create mode 100644 addons/mcs_operating_system/views/project_views.xml create mode 100644 addons/mcs_operating_system/views/timesheet_views.xml diff --git a/addons/mcs_operating_system/__init__.py b/addons/mcs_operating_system/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/addons/mcs_operating_system/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/mcs_operating_system/__manifest__.py b/addons/mcs_operating_system/__manifest__.py new file mode 100644 index 0000000..f54e264 --- /dev/null +++ b/addons/mcs_operating_system/__manifest__.py @@ -0,0 +1,35 @@ +{ + "name": "Metatroncube Operating System", + "version": "17.0.1.0.0", + "category": "Productivity", + "summary": "Revenue, delivery, capacity, and profitability operating system", + "author": "Metatroncube Software Solutions", + "license": "LGPL-3", + "depends": [ + "account", + "calendar", + "contacts", + "crm", + "hr", + "hr_timesheet", + "project", + "sale_management", + ], + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "data/crm_data.xml", + "data/project_data.xml", + "data/operating_data.xml", + "data/cron_data.xml", + "views/crm_lead_views.xml", + "views/project_views.xml", + "views/timesheet_views.xml", + "views/hr_employee_views.xml", + "views/operating_views.xml", + "views/dashboard_views.xml", + "views/menu.xml", + ], + "installable": True, + "application": True, +} diff --git a/addons/mcs_operating_system/data/crm_data.xml b/addons/mcs_operating_system/data/crm_data.xml new file mode 100644 index 0000000..7e13271 --- /dev/null +++ b/addons/mcs_operating_system/data/crm_data.xml @@ -0,0 +1,62 @@ + + + Target + 10 + + + Contacted + 20 + + + Replied + 30 + + + Discovery + 40 + + + Qualified + 50 + + + Proposal + 60 + + + Negotiation + 70 + + + Won + 80 + 1 + 1 + + + Lost + 90 + 1 + + + Referral + Existing Client + LinkedIn + Cold Email + Cold Call + Website + Google + Facebook + Instagram + Networking + Agency Partner + Canada + India + Zambia + Other + + Revenue Website Sprint + Growth Retainer + MVP Launch Sprint + White-Label Agency Partnership + diff --git a/addons/mcs_operating_system/data/cron_data.xml b/addons/mcs_operating_system/data/cron_data.xml new file mode 100644 index 0000000..f975a5a --- /dev/null +++ b/addons/mcs_operating_system/data/cron_data.xml @@ -0,0 +1,11 @@ + + + MCS: Schedule Missing CRM Follow-Ups + + code + model._cron_mcs_schedule_missing_followups() + 1 + days + 1 + + diff --git a/addons/mcs_operating_system/data/operating_data.xml b/addons/mcs_operating_system/data/operating_data.xml new file mode 100644 index 0000000..638bfb6 --- /dev/null +++ b/addons/mcs_operating_system/data/operating_data.xml @@ -0,0 +1,27 @@ + + + CEO Dashboard + + + + Close enough revenue to cover monthly payroll gap + + p0 + Set current monthly cash gap and assign close targets. + + + + Complete ClicksToCart final release + + p0 + Confirm P0/P1 launch blockers. + + + + Complete Hondavert current development milestone + + p1 + Confirm scope and assign feature deliverables. + + + diff --git a/addons/mcs_operating_system/data/project_data.xml b/addons/mcs_operating_system/data/project_data.xml new file mode 100644 index 0000000..8dfe0cd --- /dev/null +++ b/addons/mcs_operating_system/data/project_data.xml @@ -0,0 +1,117 @@ + + + Planned + 10 + + + Ready + 20 + + + In Progress + 30 + + + Review + 40 + + + Client Review + 50 + + + Blocked + 60 + + + Ready to Deploy + 70 + + + Completed + 80 + 1 + + + Frontend + Backend + Mobile + API + QA + Deployment + Client Feedback + Documentation + + + 30-Day Revenue Sprint + + 1 + 1 + sales + active + 1 + Build current prospect list and assign follow-ups. + + + Week 1 - Pipeline Build + + + + Week 2 - Conversations + + + + Week 3 - Proposal + Follow-Up + + + + Week 4 - Close + Collect + + + + + ClicksToCart - Final Release + + 1 + 1 + client + finish_now + 1 + Lock final release scope and confirm launch blockers. + + Release Scope Locked + P0/P1 Bugs Resolved + QA Completed + UAT / Client Approval + Production Deployment + Post-Launch Monitoring + Project Closed + + + Hondavert - Current Development Phase + + 1 + 1 + client + finish_now + 1 + Confirm current scope and assign deliverable-based tasks. + + Current Scope Confirmed + Core Development + Internal QA + Client Review + Fixes + Staging Approval + Production Deployment + Phase Closed + + + Idea Parking Lot + + 0 + parking_lot + parked + Review parked ideas twice per week. + + diff --git a/addons/mcs_operating_system/models/__init__.py b/addons/mcs_operating_system/models/__init__.py new file mode 100644 index 0000000..1291471 --- /dev/null +++ b/addons/mcs_operating_system/models/__init__.py @@ -0,0 +1,7 @@ +from . import crm_lead +from . import dashboard +from . import hr_employee +from . import operating +from . import project +from . import sale_order +from . import timesheet diff --git a/addons/mcs_operating_system/models/crm_lead.py b/addons/mcs_operating_system/models/crm_lead.py new file mode 100644 index 0000000..0cdebad --- /dev/null +++ b/addons/mcs_operating_system/models/crm_lead.py @@ -0,0 +1,110 @@ +from odoo import api, fields, models + + +class CrmLead(models.Model): + _inherit = "crm.lead" + + mcs_service_offer = fields.Selection( + [ + ("revenue_website_sprint", "Revenue Website Sprint"), + ("growth_retainer", "Growth Retainer"), + ("mvp_launch_sprint", "MVP Launch Sprint"), + ("white_label_agency", "White-Label Agency Partnership"), + ("other", "Other"), + ], + string="Service Interest", + tracking=True, + ) + mcs_expected_monthly_revenue = fields.Monetary( + string="Expected Monthly Revenue", tracking=True + ) + mcs_one_time_project_value = fields.Monetary( + string="One-Time Project Value", tracking=True + ) + mcs_proposal_sent_date = fields.Date(string="Proposal Sent Date", tracking=True) + mcs_last_contact_date = fields.Date(string="Last Contact Date", tracking=True) + mcs_followup_date = fields.Date(string="Follow-Up Date", tracking=True) + mcs_next_action = fields.Char(string="Next Action", tracking=True) + mcs_revenue_priority = fields.Selection( + [ + ("existing_upsell", "Existing Client Upsell"), + ("existing_referral", "Existing Client Referral"), + ("canadian_client", "Canadian Client"), + ("canadian_agency", "Canadian Agency Partnership"), + ("warm_network", "Warm Network"), + ("high_value_outbound", "High-Value Outbound"), + ("india", "India Opportunity"), + ("low_ticket", "Smaller Low-Ticket Retainer"), + ], + string="Revenue Priority", + tracking=True, + ) + mcs_has_next_activity = fields.Boolean( + string="Has Next Activity", compute="_compute_mcs_operating_flags", store=True + ) + mcs_missing_operating_data = fields.Boolean( + string="Missing Operating Data", compute="_compute_mcs_operating_flags", store=True + ) + mcs_operating_notes = fields.Text(string="Operating Notes") + + @api.depends( + "activity_ids", + "mcs_next_action", + "mcs_followup_date", + "date_deadline", + "expected_revenue", + "mcs_expected_monthly_revenue", + "mcs_one_time_project_value", + "user_id", + "type", + "active", + "probability", + ) + def _compute_mcs_operating_flags(self): + for lead in self: + is_open_opportunity = ( + lead.active and lead.type == "opportunity" and lead.probability < 100 + ) + lead.mcs_has_next_activity = bool( + lead.activity_ids or lead.mcs_next_action or lead.mcs_followup_date + ) + has_value = bool( + lead.expected_revenue + or lead.mcs_expected_monthly_revenue + or lead.mcs_one_time_project_value + ) + lead.mcs_missing_operating_data = bool( + is_open_opportunity + and ( + not lead.user_id + or not lead.date_deadline + or not lead.mcs_has_next_activity + or not has_value + ) + ) + + def action_mcs_schedule_followup(self): + todo = self.env.ref("mail.mail_activity_data_todo") + today = fields.Date.context_today(self) + for lead in self: + if lead.activity_ids: + continue + lead.activity_schedule( + todo.id, + date_deadline=lead.mcs_followup_date or lead.date_deadline or today, + summary=lead.mcs_next_action or "Follow up", + user_id=lead.user_id.id or self.env.user.id, + ) + return True + + def _cron_mcs_schedule_missing_followups(self): + leads = self.search( + [ + ("type", "=", "opportunity"), + ("active", "=", True), + ("probability", "<", 100), + ("activity_ids", "=", False), + ], + limit=100, + ) + leads.action_mcs_schedule_followup() diff --git a/addons/mcs_operating_system/models/dashboard.py b/addons/mcs_operating_system/models/dashboard.py new file mode 100644 index 0000000..434f8f8 --- /dev/null +++ b/addons/mcs_operating_system/models/dashboard.py @@ -0,0 +1,174 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class McsOperatingDashboard(models.Model): + _name = "mcs.operating.dashboard" + _description = "Operating Dashboard" + + name = fields.Char(default="CEO Dashboard", required=True) + currency_id = fields.Many2one( + "res.currency", default=lambda self: self.env.company.currency_id + ) + month_start = fields.Date(compute="_compute_metrics") + month_end = fields.Date(compute="_compute_metrics") + monthly_revenue_target = fields.Monetary(compute="_compute_metrics") + revenue_closed_month = fields.Monetary(compute="_compute_metrics") + monthly_cash_gap = fields.Monetary(compute="_compute_metrics") + new_mrr_won = fields.Monetary(compute="_compute_metrics") + pipeline_value = fields.Monetary(compute="_compute_metrics") + weighted_pipeline = fields.Monetary(compute="_compute_metrics") + proposals_outstanding = fields.Integer(compute="_compute_metrics") + deals_expected_month = fields.Integer(compute="_compute_metrics") + cash_collected = fields.Monetary(compute="_compute_metrics") + invoices_overdue = fields.Integer(compute="_compute_metrics") + active_clients = fields.Integer(compute="_compute_metrics") + opportunities_created = fields.Integer(compute="_compute_metrics") + opportunities_won = fields.Integer(compute="_compute_metrics") + conversion_rate = fields.Float(compute="_compute_metrics") + followups_due_today = fields.Integer(compute="_compute_metrics") + projects_at_risk = fields.Integer(compute="_compute_metrics") + blockers = fields.Integer(compute="_compute_metrics") + overdue_tasks = fields.Integer(compute="_compute_metrics") + team_capacity_hours = fields.Float(compute="_compute_metrics") + planned_hours = fields.Float(compute="_compute_metrics") + actual_hours = fields.Float(compute="_compute_metrics") + billable_hours = fields.Float(compute="_compute_metrics") + non_billable_hours = fields.Float(compute="_compute_metrics") + utilization = fields.Float(compute="_compute_metrics") + + @api.depends_context("uid") + def _compute_metrics(self): + today = fields.Date.context_today(self) + month_start = today.replace(day=1) + month_end = month_start + relativedelta(months=1, days=-1) + Lead = self.env["crm.lead"] + Project = self.env["project.project"] + Task = self.env["project.task"] + Timesheet = self.env["account.analytic.line"] + Invoice = self.env["account.move"] + Employee = self.env["hr.employee"] + + open_pipeline = Lead.search( + [("type", "=", "opportunity"), ("active", "=", True), ("probability", "<", 100)] + ) + won_month = Lead.search( + [ + ("type", "=", "opportunity"), + ("active", "=", True), + ("probability", "=", 100), + ("date_closed", ">=", month_start), + ("date_closed", "<=", month_end), + ] + ) + invoices_month = Invoice.search( + [ + ("move_type", "=", "out_invoice"), + ("state", "=", "posted"), + ("invoice_date", ">=", month_start), + ("invoice_date", "<=", month_end), + ] + ) + overdue_invoices = Invoice.search( + [ + ("move_type", "=", "out_invoice"), + ("state", "=", "posted"), + ("payment_state", "not in", ["paid", "in_payment", "reversed"]), + ("invoice_date_due", "<", today), + ] + ) + timesheets_month = Timesheet.search( + [("date", ">=", month_start), ("date", "<=", month_end)] + ) + active_projects = Project.search([("active", "=", True)]) + active_tasks = Task.search([("active", "=", True)]) + employees = Employee.search([("active", "=", True)]) + + for dashboard in self: + dashboard.month_start = month_start + dashboard.month_end = month_end + dashboard.monthly_revenue_target = sum( + active_projects.mapped("mcs_monthly_revenue_target") + ) + dashboard.revenue_closed_month = sum(won_month.mapped("expected_revenue")) + dashboard.monthly_cash_gap = ( + dashboard.monthly_revenue_target - dashboard.revenue_closed_month + ) + dashboard.new_mrr_won = sum(won_month.mapped("mcs_expected_monthly_revenue")) + dashboard.pipeline_value = sum(open_pipeline.mapped("expected_revenue")) + dashboard.weighted_pipeline = sum( + lead.expected_revenue * lead.probability / 100 for lead in open_pipeline + ) + dashboard.proposals_outstanding = Lead.search_count( + [ + ("type", "=", "opportunity"), + ("active", "=", True), + ("mcs_proposal_sent_date", "!=", False), + ("probability", "<", 100), + ] + ) + dashboard.deals_expected_month = Lead.search_count( + [ + ("type", "=", "opportunity"), + ("active", "=", True), + ("date_deadline", ">=", month_start), + ("date_deadline", "<=", month_end), + ("probability", "<", 100), + ] + ) + dashboard.cash_collected = sum( + invoice.amount_total - invoice.amount_residual for invoice in invoices_month + ) + dashboard.invoices_overdue = len(overdue_invoices) + dashboard.active_clients = len(set(active_projects.mapped("partner_id").ids)) + created = Lead.search_count( + [ + ("type", "=", "opportunity"), + ("create_date", ">=", month_start), + ("create_date", "<=", month_end), + ] + ) + dashboard.opportunities_created = created + dashboard.opportunities_won = len(won_month) + dashboard.conversion_rate = len(won_month) / created * 100 if created else 0 + dashboard.followups_due_today = self.env["mail.activity"].search_count( + [ + ("user_id", "=", self.env.user.id), + ("date_deadline", "<=", today), + ("res_model", "=", "crm.lead"), + ] + ) + dashboard.projects_at_risk = len(active_projects.filtered("mcs_at_risk")) + dashboard.blockers = len(active_tasks.filtered("mcs_is_blocked")) + dashboard.overdue_tasks = len( + active_tasks.filtered( + lambda task: task.date_deadline + and fields.Date.to_date(task.date_deadline) < today + and not (task.stage_id and task.stage_id.fold) + ) + ) + dashboard.team_capacity_hours = sum(employees.mapped("mcs_available_hours_week")) + dashboard.planned_hours = sum(active_tasks.mapped("allocated_hours")) + dashboard.actual_hours = sum(timesheets_month.mapped("unit_amount")) + dashboard.billable_hours = sum( + timesheets_month.filtered( + lambda line: line.mcs_billable_classification == "billable" + ).mapped("unit_amount") + ) + dashboard.non_billable_hours = dashboard.actual_hours - dashboard.billable_hours + dashboard.utilization = ( + dashboard.billable_hours / dashboard.team_capacity_hours * 100 + if dashboard.team_capacity_hours + else 0 + ) + + def action_refresh(self): + return { + "type": "ir.actions.act_window", + "name": "CEO Dashboard", + "res_model": "mcs.operating.dashboard", + "view_mode": "form", + "target": "current", + "context": {"create": False}, + } diff --git a/addons/mcs_operating_system/models/hr_employee.py b/addons/mcs_operating_system/models/hr_employee.py new file mode 100644 index 0000000..430d20b --- /dev/null +++ b/addons/mcs_operating_system/models/hr_employee.py @@ -0,0 +1,20 @@ +from odoo import fields, models + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + mcs_operating_role = fields.Selection( + [ + ("founder", "Founder"), + ("senior_developer", "Senior Developer"), + ("junior_developer", "Junior Developer"), + ("designer", "Graphic Designer"), + ("video_editor", "Video Editor"), + ("seo_specialist", "SEO Specialist"), + ("ads_specialist", "Ads Specialist"), + ("operations", "Operations"), + ], + string="Operating Role", + ) + mcs_available_hours_week = fields.Float(string="Available Hours / Week", default=40) diff --git a/addons/mcs_operating_system/models/operating.py b/addons/mcs_operating_system/models/operating.py new file mode 100644 index 0000000..c4c9f78 --- /dev/null +++ b/addons/mcs_operating_system/models/operating.py @@ -0,0 +1,235 @@ +from odoo import api, fields, models + + +class McsCompanyGoal(models.Model): + _name = "mcs.company.goal" + _description = "Company Goal" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "sequence, date_deadline, id" + + name = fields.Char(required=True, tracking=True) + sequence = fields.Integer(default=10) + owner_id = fields.Many2one("res.users", string="Owner", default=lambda self: self.env.user) + date_deadline = fields.Date(string="Deadline", tracking=True) + state = fields.Selection( + [ + ("planned", "Planned"), + ("active", "Active"), + ("blocked", "Blocked"), + ("done", "Done"), + ("cancelled", "Cancelled"), + ], + default="active", + required=True, + tracking=True, + ) + priority = fields.Selection( + [("p0", "P0"), ("p1", "P1"), ("p2", "P2"), ("p3", "P3")], + default="p1", + required=True, + ) + target_amount = fields.Monetary(string="Target Amount") + currency_id = fields.Many2one( + "res.currency", default=lambda self: self.env.company.currency_id + ) + next_action = fields.Char(string="Next Action") + blocker = fields.Text() + project_ids = fields.Many2many("project.project", string="Projects / Initiatives") + opportunity_ids = fields.Many2many("crm.lead", string="Opportunities") + notes = fields.Html() + + def action_done(self): + self.write({"state": "done"}) + + def action_blocked(self): + self.write({"state": "blocked"}) + + def action_active(self): + self.write({"state": "active"}) + + +class McsIdeaParking(models.Model): + _name = "mcs.idea.parking" + _description = "Idea Parking Lot" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "review_date, urgency desc, id desc" + + name = fields.Char(string="Idea", required=True, tracking=True) + date_added = fields.Date(default=fields.Date.context_today, required=True) + potential_value = fields.Selection( + [("low", "Low"), ("medium", "Medium"), ("high", "High"), ("strategic", "Strategic")], + default="medium", + ) + urgency = fields.Selection( + [("low", "Low"), ("medium", "Medium"), ("high", "High"), ("urgent", "Urgent")], + default="low", + ) + owner_id = fields.Many2one("res.users", string="Owner", default=lambda self: self.env.user) + decision = fields.Selection( + [ + ("pending", "Pending"), + ("approve", "Approve"), + ("park", "Park"), + ("kill", "Kill"), + ], + default="pending", + required=True, + tracking=True, + ) + review_date = fields.Date(string="Review Date") + project_id = fields.Many2one("project.project", string="Converted Project") + notes = fields.Html() + + def action_approve(self): + self.write({"decision": "approve"}) + + def action_park(self): + self.write({"decision": "park"}) + + def action_kill(self): + self.write({"decision": "kill"}) + + +class McsOperatingReview(models.Model): + _name = "mcs.operating.review" + _description = "Operating Review" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "review_date desc, id desc" + + name = fields.Char(compute="_compute_name", store=True) + review_type = fields.Selection( + [ + ("daily", "Daily Operating Meeting"), + ("monday", "Monday Commitments"), + ("wednesday", "Wednesday Revenue Review"), + ("friday", "Friday Operating Review"), + ("monthly", "Monthly Operating Review"), + ], + required=True, + default="daily", + tracking=True, + ) + review_date = fields.Date(default=fields.Date.context_today, required=True) + owner_id = fields.Many2one("res.users", string="Owner", default=lambda self: self.env.user) + shipped_yesterday = fields.Html() + ships_today = fields.Html() + blockers = fields.Html() + commitments = fields.Html() + revenue_notes = fields.Html() + delivery_notes = fields.Html() + capacity_notes = fields.Html() + cash_notes = fields.Html() + decisions = fields.Html() + next_actions = fields.Html() + + @api.depends("review_type", "review_date") + def _compute_name(self): + labels = dict(self._fields["review_type"].selection) + for review in self: + review.name = "%s - %s" % ( + labels.get(review.review_type, "Operating Review"), + review.review_date or fields.Date.context_today(review), + ) + + +class McsSalesTarget(models.Model): + _name = "mcs.sales.target" + _description = "Sales Target" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "target_date desc, user_id" + + name = fields.Char(compute="_compute_name", store=True) + target_date = fields.Date(default=fields.Date.context_today, required=True) + user_id = fields.Many2one("res.users", string="Owner", default=lambda self: self.env.user) + outbound_target = fields.Integer(default=25) + outbound_done = fields.Integer() + followup_target = fields.Integer(default=8) + followup_done = fields.Integer() + call_target = fields.Integer(default=3) + call_done = fields.Integer() + proposal_target = fields.Integer(default=1) + proposal_done = fields.Integer() + referral_target = fields.Integer(default=2) + referral_done = fields.Integer() + notes = fields.Html() + + @api.depends("target_date", "user_id") + def _compute_name(self): + for target in self: + target.name = "%s - %s" % ( + target.user_id.name or "Sales", + target.target_date or fields.Date.context_today(target), + ) + + +class McsClientProfitability(models.Model): + _name = "mcs.client.profitability" + _description = "Client Profitability" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "period_start desc, partner_id" + + name = fields.Char(compute="_compute_name", store=True) + partner_id = fields.Many2one("res.partner", string="Client", required=True) + project_id = fields.Many2one("project.project", string="Project") + period_start = fields.Date(required=True) + period_end = fields.Date(required=True) + currency_id = fields.Many2one( + "res.currency", default=lambda self: self.env.company.currency_id + ) + client_revenue = fields.Monetary() + delivery_hours = fields.Float() + account_management_hours = fields.Float() + rework_hours = fields.Float() + revision_hours = fields.Float() + internal_hourly_cost = fields.Monetary(default=0) + internal_delivery_cost = fields.Monetary( + compute="_compute_profitability", store=True + ) + gross_contribution = fields.Monetary(compute="_compute_profitability", store=True) + gross_margin = fields.Float(compute="_compute_profitability", store=True) + recommendation = fields.Selection( + [ + ("keep", "Keep"), + ("reprice", "Reprice"), + ("rescope", "Rescope"), + ("automate", "Automate"), + ("reduce", "Reduce"), + ("exit", "Exit"), + ], + default="keep", + tracking=True, + ) + notes = fields.Html() + + @api.depends("partner_id", "period_start", "period_end") + def _compute_name(self): + for record in self: + record.name = "%s - %s to %s" % ( + record.partner_id.display_name or "Client", + record.period_start or "", + record.period_end or "", + ) + + @api.depends( + "delivery_hours", + "account_management_hours", + "rework_hours", + "revision_hours", + "internal_hourly_cost", + "client_revenue", + ) + def _compute_profitability(self): + for record in self: + hours = ( + record.delivery_hours + + record.account_management_hours + + record.rework_hours + + record.revision_hours + ) + record.internal_delivery_cost = hours * record.internal_hourly_cost + record.gross_contribution = record.client_revenue - record.internal_delivery_cost + record.gross_margin = ( + record.gross_contribution / record.client_revenue * 100 + if record.client_revenue + else 0 + ) diff --git a/addons/mcs_operating_system/models/project.py b/addons/mcs_operating_system/models/project.py new file mode 100644 index 0000000..e0925ea --- /dev/null +++ b/addons/mcs_operating_system/models/project.py @@ -0,0 +1,146 @@ +from odoo import api, fields, models + + +WORK_CATEGORY_SELECTION = [ + ("billable_delivery", "Billable Delivery"), + ("internal_product", "Internal Product"), + ("sales_support", "Sales Support"), + ("rework", "Rework"), + ("client_support", "Client Support"), + ("admin", "Admin"), + ("meetings", "Meetings"), + ("qa", "QA"), + ("research", "Research"), +] + + +class ProjectProject(models.Model): + _inherit = "project.project" + + mcs_initiative_type = fields.Selection( + [ + ("client", "Client Engagement"), + ("internal", "Internal Initiative"), + ("product", "Product Development"), + ("sales", "Sales Execution"), + ("parking_lot", "Idea Parking Lot"), + ], + string="Initiative Type", + tracking=True, + ) + mcs_classification = fields.Selection( + [ + ("finish_now", "Finish Now"), + ("active", "Active"), + ("parked", "Parked"), + ("kill", "Kill"), + ], + string="Operating Classification", + default="active", + tracking=True, + ) + mcs_revenue_connected = fields.Boolean(string="Connected to Revenue", tracking=True) + mcs_monthly_revenue_target = fields.Monetary(string="Monthly Revenue Target") + mcs_contracted_revenue = fields.Monetary(string="Contracted Revenue") + mcs_cash_gap = fields.Monetary(string="Cash Gap", compute="_compute_mcs_cash_gap") + mcs_planned_hours = fields.Float( + string="Planned Hours", compute="_compute_mcs_project_hours", store=True + ) + mcs_actual_hours = fields.Float( + string="Actual Hours", compute="_compute_mcs_project_hours", store=True + ) + mcs_utilization = fields.Float( + string="Planned vs Actual %", compute="_compute_mcs_project_hours", store=True + ) + mcs_next_action = fields.Char(string="Next Action", tracking=True) + mcs_blocker = fields.Text(string="Blocker", tracking=True) + mcs_at_risk = fields.Boolean( + string="At Risk", compute="_compute_mcs_at_risk", store=True + ) + + @api.depends("mcs_monthly_revenue_target", "mcs_contracted_revenue") + def _compute_mcs_cash_gap(self): + for project in self: + project.mcs_cash_gap = ( + project.mcs_monthly_revenue_target - project.mcs_contracted_revenue + ) + + @api.depends("tasks.allocated_hours", "tasks.effective_hours") + def _compute_mcs_project_hours(self): + for project in self: + planned = sum(project.tasks.mapped("allocated_hours")) + actual = sum(project.tasks.mapped("effective_hours")) + project.mcs_planned_hours = planned + project.mcs_actual_hours = actual + project.mcs_utilization = actual / planned * 100 if planned else 0 + + @api.depends("tasks.mcs_at_risk", "mcs_blocker", "mcs_next_action") + def _compute_mcs_at_risk(self): + for project in self: + project.mcs_at_risk = bool( + project.mcs_blocker or not project.mcs_next_action or any(project.tasks.mapped("mcs_at_risk")) + ) + + +class ProjectTask(models.Model): + _inherit = "project.task" + + mcs_priority = fields.Selection( + [ + ("p0", "P0 - Critical"), + ("p1", "P1 - High"), + ("p2", "P2 - Normal"), + ("p3", "P3 - Low"), + ], + string="Operating Priority", + default="p2", + tracking=True, + ) + mcs_definition_of_done = fields.Html(string="Definition of Done") + mcs_next_action = fields.Char(string="Next Action", tracking=True) + mcs_blocker = fields.Text(string="Blocker", tracking=True) + mcs_client_internal = fields.Selection( + [("client", "Client"), ("internal", "Internal")], + string="Client / Internal", + default="client", + tracking=True, + ) + mcs_work_category = fields.Selection(WORK_CATEGORY_SELECTION, string="Work Category") + mcs_is_blocked = fields.Boolean(string="Blocked", compute="_compute_mcs_flags", store=True) + mcs_at_risk = fields.Boolean(string="At Risk", compute="_compute_mcs_flags", store=True) + mcs_missing_operating_data = fields.Boolean( + string="Missing Operating Data", compute="_compute_mcs_flags", store=True + ) + + @api.depends( + "user_ids", + "date_deadline", + "allocated_hours", + "mcs_definition_of_done", + "mcs_next_action", + "mcs_blocker", + "depend_on_ids", + "stage_id.fold", + ) + def _compute_mcs_flags(self): + today = fields.Date.context_today(self) + for task in self: + is_done = bool(task.stage_id and task.stage_id.fold) + has_deadline_passed = bool( + task.date_deadline and fields.Date.to_date(task.date_deadline) < today + ) + task.mcs_is_blocked = bool(task.mcs_blocker or task.depend_on_ids) + task.mcs_missing_operating_data = bool( + not is_done + and ( + not task.user_ids + or not task.date_deadline + or not task.allocated_hours + or not task.mcs_definition_of_done + or not task.mcs_next_action + ) + ) + task.mcs_at_risk = bool( + not is_done + and (task.mcs_is_blocked or has_deadline_passed or task.mcs_missing_operating_data) + ) diff --git a/addons/mcs_operating_system/models/sale_order.py b/addons/mcs_operating_system/models/sale_order.py new file mode 100644 index 0000000..9e203e1 --- /dev/null +++ b/addons/mcs_operating_system/models/sale_order.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + mcs_service_offer = fields.Selection( + related="opportunity_id.mcs_service_offer", string="Service Offer", store=True + ) + mcs_revenue_priority = fields.Selection( + related="opportunity_id.mcs_revenue_priority", string="Revenue Priority", store=True + ) diff --git a/addons/mcs_operating_system/models/timesheet.py b/addons/mcs_operating_system/models/timesheet.py new file mode 100644 index 0000000..6c4380c --- /dev/null +++ b/addons/mcs_operating_system/models/timesheet.py @@ -0,0 +1,19 @@ +from odoo import fields, models + +from .project import WORK_CATEGORY_SELECTION + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + mcs_work_category = fields.Selection(WORK_CATEGORY_SELECTION, string="Work Category") + mcs_billable_classification = fields.Selection( + [ + ("billable", "Billable"), + ("non_billable", "Non-Billable"), + ("internal", "Internal"), + ], + string="Billable Classification", + default="billable", + ) + mcs_is_rework = fields.Boolean(string="Rework") diff --git a/addons/mcs_operating_system/security/ir.model.access.csv b/addons/mcs_operating_system/security/ir.model.access.csv new file mode 100644 index 0000000..96fce5d --- /dev/null +++ b/addons/mcs_operating_system/security/ir.model.access.csv @@ -0,0 +1,12 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mcs_company_goal_user,mcs.company.goal.user,model_mcs_company_goal,mcs_operating_system.group_mcs_operating_user,1,1,1,0 +access_mcs_company_goal_manager,mcs.company.goal.manager,model_mcs_company_goal,mcs_operating_system.group_mcs_operating_manager,1,1,1,1 +access_mcs_idea_parking_user,mcs.idea.parking.user,model_mcs_idea_parking,mcs_operating_system.group_mcs_operating_user,1,1,1,0 +access_mcs_idea_parking_manager,mcs.idea.parking.manager,model_mcs_idea_parking,mcs_operating_system.group_mcs_operating_manager,1,1,1,1 +access_mcs_operating_review_user,mcs.operating.review.user,model_mcs_operating_review,mcs_operating_system.group_mcs_operating_user,1,1,1,0 +access_mcs_operating_review_manager,mcs.operating.review.manager,model_mcs_operating_review,mcs_operating_system.group_mcs_operating_manager,1,1,1,1 +access_mcs_sales_target_user,mcs.sales.target.user,model_mcs_sales_target,mcs_operating_system.group_mcs_operating_user,1,1,1,0 +access_mcs_sales_target_manager,mcs.sales.target.manager,model_mcs_sales_target,mcs_operating_system.group_mcs_operating_manager,1,1,1,1 +access_mcs_client_profitability_user,mcs.client.profitability.user,model_mcs_client_profitability,mcs_operating_system.group_mcs_operating_user,1,1,1,0 +access_mcs_client_profitability_manager,mcs.client.profitability.manager,model_mcs_client_profitability,mcs_operating_system.group_mcs_operating_manager,1,1,1,1 +access_mcs_operating_dashboard_user,mcs.operating.dashboard.user,model_mcs_operating_dashboard,mcs_operating_system.group_mcs_operating_user,1,0,0,0 diff --git a/addons/mcs_operating_system/security/security.xml b/addons/mcs_operating_system/security/security.xml new file mode 100644 index 0000000..d7ab74f --- /dev/null +++ b/addons/mcs_operating_system/security/security.xml @@ -0,0 +1,12 @@ + + + Operating System User + + + + + Operating System Manager + + + + diff --git a/addons/mcs_operating_system/views/crm_lead_views.xml b/addons/mcs_operating_system/views/crm_lead_views.xml new file mode 100644 index 0000000..2d5a5bc --- /dev/null +++ b/addons/mcs_operating_system/views/crm_lead_views.xml @@ -0,0 +1,59 @@ + + + crm.lead.form.mcs.operating + crm.lead + + + +