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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ crm.lead.tree.mcs.operating
+ crm.lead
+
+
+
+ mcs_missing_operating_data
+
+
+
+
+
+
+
+
+
+
+
+
+ Revenue Board
+ crm.lead
+ kanban,tree,form,calendar,pivot,graph
+ [('type','=','opportunity')]
+ {'default_type': 'opportunity', 'search_default_assigned_to_me': 1}
+
+
diff --git a/addons/mcs_operating_system/views/dashboard_views.xml b/addons/mcs_operating_system/views/dashboard_views.xml
new file mode 100644
index 0000000..0cc1d21
--- /dev/null
+++ b/addons/mcs_operating_system/views/dashboard_views.xml
@@ -0,0 +1,67 @@
+
+
+ mcs.operating.dashboard.form
+ mcs.operating.dashboard
+
+
+
+
+
+
+ CEO Dashboard
+ mcs.operating.dashboard
+ form
+
+
+ current
+ {'create': False}
+
+
diff --git a/addons/mcs_operating_system/views/hr_employee_views.xml b/addons/mcs_operating_system/views/hr_employee_views.xml
new file mode 100644
index 0000000..fbd4418
--- /dev/null
+++ b/addons/mcs_operating_system/views/hr_employee_views.xml
@@ -0,0 +1,13 @@
+
+
+ hr.employee.form.mcs.operating
+ hr.employee
+
+
+
+
+
+
+
+
+
diff --git a/addons/mcs_operating_system/views/menu.xml b/addons/mcs_operating_system/views/menu.xml
new file mode 100644
index 0000000..42c378e
--- /dev/null
+++ b/addons/mcs_operating_system/views/menu.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mcs_operating_system/views/operating_views.xml b/addons/mcs_operating_system/views/operating_views.xml
new file mode 100644
index 0000000..33f4c89
--- /dev/null
+++ b/addons/mcs_operating_system/views/operating_views.xml
@@ -0,0 +1,314 @@
+
+
+ mcs.company.goal.tree
+ mcs.company.goal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mcs.company.goal.form
+ mcs.company.goal
+
+
+
+
+
+
+ Company Goals
+ mcs.company.goal
+ tree,form
+
+
+
+ mcs.idea.parking.tree
+ mcs.idea.parking
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mcs.idea.parking.form
+ mcs.idea.parking
+
+
+
+
+
+
+ Idea Parking Lot
+ mcs.idea.parking
+ tree,form
+
+
+
+ mcs.operating.review.tree
+ mcs.operating.review
+
+
+
+
+
+
+
+
+
+
+
+ mcs.operating.review.form
+ mcs.operating.review
+
+
+
+
+
+
+ Operating Reviews
+ mcs.operating.review
+ tree,form
+
+
+
+ mcs.sales.target.tree
+ mcs.sales.target
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mcs.sales.target.form
+ mcs.sales.target
+
+
+
+
+
+
+ Sales Targets
+ mcs.sales.target
+ tree,form,pivot,graph
+
+
+
+ mcs.client.profitability.tree
+ mcs.client.profitability
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mcs.client.profitability.form
+ mcs.client.profitability
+
+
+
+
+
+
+ Client Profitability
+ mcs.client.profitability
+ tree,form,pivot,graph
+
+
diff --git a/addons/mcs_operating_system/views/project_views.xml b/addons/mcs_operating_system/views/project_views.xml
new file mode 100644
index 0000000..2101819
--- /dev/null
+++ b/addons/mcs_operating_system/views/project_views.xml
@@ -0,0 +1,103 @@
+
+
+ project.project.form.mcs.operating
+ project.project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ project.task.form.mcs.operating
+ project.task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ project.task.tree.mcs.operating
+ project.task
+
+
+
+ mcs_at_risk
+ mcs_is_blocked
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ship Board
+ project.task
+ tree,kanban,form,calendar,pivot,graph
+ [('active','=',True)]
+ {'search_default_open_tasks': 1, 'group_by': 'project_id'}
+
+
+
+ Operating Projects
+ project.project
+ kanban,tree,form
+ [('active','=',True)]
+
+
diff --git a/addons/mcs_operating_system/views/timesheet_views.xml b/addons/mcs_operating_system/views/timesheet_views.xml
new file mode 100644
index 0000000..0dec497
--- /dev/null
+++ b/addons/mcs_operating_system/views/timesheet_views.xml
@@ -0,0 +1,34 @@
+
+
+ account.analytic.line.form.mcs.operating
+ account.analytic.line
+
+
+
+
+
+
+
+
+
+
+
+ account.analytic.line.tree.mcs.operating
+ account.analytic.line
+
+
+
+
+
+
+
+
+
+
+
+ Capacity Board
+ account.analytic.line
+ tree,pivot,graph,form
+ {'group_by': 'employee_id'}
+
+