diff --git a/addons/mcs_operating_system/hooks.py b/addons/mcs_operating_system/hooks.py index 5df4685..68dbf18 100644 --- a/addons/mcs_operating_system/hooks.py +++ b/addons/mcs_operating_system/hooks.py @@ -16,56 +16,118 @@ class OperatingSystemSetup: self.Project = env["project.project"].sudo() def run(self): + self._promote_existing_customers() self._ensure_active_portfolio() + self._hide_empty_seed_accounts() self._ensure_followup_cron_active() + def _promote_existing_customers(self): + invoice_partners = self.env["account.move"].sudo().search( + [ + ("move_type", "=", "out_invoice"), + ("state", "=", "posted"), + ("commercial_partner_id", "!=", False), + ] + ).mapped("commercial_partner_id") + customers = self.Partner.search([("customer_rank", ">", 0)]) + for partner in customers | invoice_partners: + values = {"mcs_is_client_account": True} + if not partner.mcs_account_status: + values["mcs_account_status"] = "active" + if partner.currency_id and not partner.mcs_currency_id: + values["mcs_currency_id"] = partner.currency_id.id + partner.write(values) + def _ensure_active_portfolio(self): specs = { - "ClicksToCart": [ - ("ERP Implementation", "fixed_project"), - ("App Development", "fixed_project"), - ("Social Media Handling", "recurring"), - ], - "Wallaceton": [ - ("SEO", "recurring"), - ("Social Media Handling", "recurring"), - ("Ads Management", "recurring"), - ], - "Maison de Treats": [("Social Media Handling", "recurring")], - "Lens and Frames": [ - ("SEO", "recurring"), - ("Social Media Handling", "recurring"), - ("Ads Management", "recurring"), - ], - "TNCSC": [ - ("Ads Management", "recurring"), - ("Videos", "fixed_project"), - ("ERP", "fixed_project"), - ], - "Infini": [("Social Media Handling", "recurring")], - "Organic Healthy Family": [("Shopify Store Development", "fixed_project")], - "Hondavert": [ - ("Website Development", "fixed_project"), - ("App Development", "fixed_project"), - ], - "RaceNation": [("Website Development", "fixed_project")], - "Racewerks": [("Website Development", "fixed_project")], - "Internal": [ - ("Shopify App for Revenue Generation", "internal_product"), - ("Custom Product Development", "internal_r_and_d"), - ], + "ClicksToCart": { + "aliases": ["Clickstocart Inc", "ClicksToCart"], + "workstreams": [ + ("ERP Implementation", "fixed_project"), + ("App Development", "fixed_project"), + ("Social Media Handling", "recurring"), + ], + }, + "Wallaceton": { + "aliases": ["Wallaceton Eye Care", "Wallaceton"], + "workstreams": [ + ("SEO", "recurring"), + ("Social Media Handling", "recurring"), + ("Ads Management", "recurring"), + ], + }, + "Maison de Treats": { + "aliases": ["Maison de Treats"], + "workstreams": [("Social Media Handling", "recurring")], + }, + "Lens and Frames": { + "aliases": ["Lens & Frames Optical", "Lens and Frames"], + "workstreams": [ + ("SEO", "recurring"), + ("Social Media Handling", "recurring"), + ("Ads Management", "recurring"), + ], + }, + "TNCSC": { + "aliases": ["Tamil Nadu Cultural Society of Canada", "TNCSC"], + "workstreams": [ + ("Ads Management", "recurring"), + ("Videos", "fixed_project"), + ("ERP", "fixed_project"), + ], + }, + "Infini": { + "aliases": ["INFINI BRAND ENHANCERZ", "Infini"], + "workstreams": [("Social Media Handling", "recurring")], + }, + "Organic Healthy Family": { + "aliases": ["Organic Healthy Family"], + "workstreams": [("Shopify Store Development", "fixed_project")], + }, + "Hondavert": { + "aliases": ["Hondavert"], + "workstreams": [ + ("Website Development", "fixed_project"), + ("App Development", "fixed_project"), + ], + }, + "RaceNation": { + "aliases": ["RaceNation"], + "workstreams": [("Website Development", "fixed_project")], + }, + "Racewerks": { + "aliases": ["Racewerks"], + "workstreams": [("Website Development", "fixed_project")], + }, + "Internal": { + "aliases": ["Internal"], + "workstreams": [ + ("Shopify App for Revenue Generation", "internal_product"), + ("Custom Product Development", "internal_r_and_d"), + ], + }, } - for account_name, workstreams in specs.items(): - partner = self._ensure_client_account(account_name) - for workstream, delivery_type in workstreams: - self._ensure_project(partner, workstream, delivery_type) + for account_name, spec in specs.items(): + partner = self._ensure_client_account(account_name, spec["aliases"]) + for workstream, delivery_type in spec["workstreams"]: + self._ensure_project(partner, account_name, workstream, delivery_type) - def _ensure_client_account(self, name): - partner = self.Partner.search([("name", "=", name)], limit=1) + def _ensure_client_account(self, name, aliases=None): + aliases = aliases or [name] + partner = self.Partner.browse() + for alias in aliases: + partner = self.Partner.search([("name", "=", alias)], limit=1) + if partner: + break + if not partner: + partner = self.Partner.search( + [("name", "ilike", aliases[0]), ("customer_rank", ">", 0)], + limit=1, + ) if not partner: partner = self.Partner.create( { - "name": name, + "name": aliases[0] if aliases else name, "company_type": "company", "mcs_is_client_account": True, "mcs_account_status": "active", @@ -77,18 +139,34 @@ class OperatingSystemSetup: values["mcs_is_client_account"] = True if not partner.mcs_account_status: values["mcs_account_status"] = "active" + if partner.currency_id and not partner.mcs_currency_id: + values["mcs_currency_id"] = partner.currency_id.id if values: partner.write(values) return partner - def _ensure_project(self, partner, workstream, delivery_type): + def _ensure_project(self, partner, account_name, workstream, delivery_type): + is_internal = partner.name == "Internal" project_name = "%s - %s" % (partner.name, workstream) - project = self.Project.search([("name", "=", project_name)], limit=1) + legacy_project_name = "%s - %s" % (account_name, workstream) + project = self.Project.search( + [ + "|", + ("name", "=", project_name), + ("name", "=", legacy_project_name), + ], + limit=1, + ) + if not project: + project = self.Project.search( + [("partner_id", "=", partner.id), ("name", "ilike", workstream)], + limit=1, + ) if not project: project = self.Project.create( { "name": project_name, - "partner_id": partner.id if partner.name != "Internal" else False, + "partner_id": partner.id if not is_internal else False, "allow_timesheets": True, "allow_milestones": True, "mcs_delivery_type": delivery_type, @@ -105,7 +183,9 @@ class OperatingSystemSetup: return project values = {} - if not project.partner_id and partner.name != "Internal": + if not project.partner_id and not is_internal: + values["partner_id"] = partner.id + if project.partner_id and project.partner_id != partner and not is_internal: values["partner_id"] = partner.id if not project.mcs_delivery_type: values["mcs_delivery_type"] = delivery_type @@ -137,3 +217,34 @@ class OperatingSystemSetup: ) if cron and not cron.active: cron.sudo().write({"active": True}) + + def _hide_empty_seed_accounts(self): + names = [ + "ClicksToCart", + "Wallaceton", + "Lens and Frames", + "TNCSC", + "Infini", + "Organic Healthy Family", + "Hondavert", + "RaceNation", + "Racewerks", + ] + for partner in self.Partner.search( + [ + ("name", "in", names), + ("customer_rank", "=", 0), + ("mcs_is_client_account", "=", True), + ] + ): + has_invoice = bool( + self.env["account.move"].sudo().search_count( + [ + ("move_type", "=", "out_invoice"), + ("state", "=", "posted"), + ("commercial_partner_id", "=", partner.commercial_partner_id.id), + ] + ) + ) + if not has_invoice and not partner.mcs_project_ids: + partner.write({"mcs_is_client_account": False}) diff --git a/addons/mcs_operating_system/models/dashboard.py b/addons/mcs_operating_system/models/dashboard.py index d34ca83..5201efb 100644 --- a/addons/mcs_operating_system/models/dashboard.py +++ b/addons/mcs_operating_system/models/dashboard.py @@ -440,15 +440,26 @@ class McsOperatingDashboard(models.Model): rows = [] for partner in partners: projects = client_projects.filtered(lambda project: project.partner_id == partner) - if not projects: + if ( + not projects + and not partner.mcs_total_invoiced + and not partner.mcs_total_outstanding + ): continue rows.append( { "id": partner.id, "name": partner.display_name, + "currency": partner.mcs_currency_id.name or "", "health": partner.mcs_account_health, "active_workstreams": len(projects), "revenue": sum(projects.mapped("mcs_monthly_revenue_target")), + "total_invoiced": partner.mcs_total_invoiced, + "outstanding": partner.mcs_total_outstanding, + "overdue": partner.mcs_total_overdue, + "last_invoice_date": fields.Date.to_string(partner.mcs_last_invoice_date) + if partner.mcs_last_invoice_date + else "", "hours": sum(projects.mapped("mcs_actual_hours")), "next_milestone": next( (project.mcs_next_milestone for project in projects if project.mcs_next_milestone), diff --git a/addons/mcs_operating_system/models/res_partner.py b/addons/mcs_operating_system/models/res_partner.py index 242335c..7131e55 100644 --- a/addons/mcs_operating_system/models/res_partner.py +++ b/addons/mcs_operating_system/models/res_partner.py @@ -58,6 +58,25 @@ class ResPartner(models.Model): compute="_compute_mcs_account_metrics", store=True, ) + mcs_total_invoiced = fields.Monetary( + string="Total Invoiced", + currency_field="mcs_currency_id", + compute="_compute_mcs_account_metrics", + ) + mcs_total_outstanding = fields.Monetary( + string="Outstanding Receivables", + currency_field="mcs_currency_id", + compute="_compute_mcs_account_metrics", + ) + mcs_total_overdue = fields.Monetary( + string="Overdue Receivables", + currency_field="mcs_currency_id", + compute="_compute_mcs_account_metrics", + ) + mcs_last_invoice_date = fields.Date( + string="Last Invoice Date", + compute="_compute_mcs_account_metrics", + ) mcs_account_owner_id = fields.Many2one( "res.users", string="Account Owner", tracking=True ) @@ -85,6 +104,10 @@ class ResPartner(models.Model): "mcs_contracted_value": "numeric", "mcs_monthly_hours": "double precision", "mcs_effective_hourly_revenue": "numeric", + "mcs_total_invoiced": "numeric", + "mcs_total_outstanding": "numeric", + "mcs_total_overdue": "numeric", + "mcs_last_invoice_date": "date", "mcs_account_owner_id": "integer", "mcs_next_account_action": "varchar", "mcs_account_risk_reason": "text", @@ -101,10 +124,31 @@ class ResPartner(models.Model): "mcs_project_ids.mcs_actual_hours", "mcs_project_ids.mcs_portfolio_health", "mcs_project_ids.active", + "currency_id", ) def _compute_mcs_account_metrics(self): + today = fields.Date.context_today(self) + Invoice = self.env["account.move"].sudo() for partner in self: projects = partner.mcs_project_ids.filtered(lambda project: project.active) + commercial_partner = partner.commercial_partner_id + invoices = Invoice.search( + [ + ("move_type", "=", "out_invoice"), + ("state", "=", "posted"), + ("commercial_partner_id", "=", commercial_partner.id), + ] + ) + overdue_invoices = invoices.filtered( + lambda invoice: invoice.invoice_date_due + and invoice.invoice_date_due < today + and invoice.payment_state not in ("paid", "in_payment", "reversed") + ) + invoice_dates = [date for date in invoices.mapped("invoice_date") if date] + partner.mcs_currency_id = ( + partner.currency_id.id + or self.env.company.currency_id.id + ) partner.mcs_monthly_revenue = sum(projects.mapped("mcs_monthly_revenue_target")) partner.mcs_contracted_value = sum(projects.mapped("mcs_contracted_revenue")) partner.mcs_monthly_hours = sum(projects.mapped("mcs_actual_hours")) @@ -113,6 +157,15 @@ class ResPartner(models.Model): if partner.mcs_monthly_hours else 0 ) + partner.mcs_total_invoiced = sum(invoices.mapped("amount_total_signed")) + partner.mcs_total_outstanding = sum( + invoices.filtered( + lambda invoice: invoice.payment_state + not in ("paid", "in_payment", "reversed") + ).mapped("amount_residual_signed") + ) + partner.mcs_total_overdue = sum(overdue_invoices.mapped("amount_residual_signed")) + partner.mcs_last_invoice_date = max(invoice_dates) if invoice_dates else False partner.mcs_account_health = partner._mcs_resolve_account_health( partner._mcs_get_account_health_signals(projects) ) diff --git a/addons/mcs_operating_system/static/src/scss/ceo_dashboard_v2.scss b/addons/mcs_operating_system/static/src/scss/ceo_dashboard_v2.scss index f6cd910..2bf3163 100644 --- a/addons/mcs_operating_system/static/src/scss/ceo_dashboard_v2.scss +++ b/addons/mcs_operating_system/static/src/scss/ceo_dashboard_v2.scss @@ -237,10 +237,10 @@ .o_mcs_table_head, .o_mcs_table_row { display: grid; - grid-template-columns: 1.3fr 92px 70px 90px 70px minmax(180px, 1.2fr); + grid-template-columns: 1.3fr 92px 70px 70px 100px 100px 100px 96px minmax(180px, 1.2fr); gap: 10px; align-items: center; - min-width: 760px; + min-width: 980px; } .o_mcs_table_head { diff --git a/addons/mcs_operating_system/static/src/xml/ceo_dashboard_v2.xml b/addons/mcs_operating_system/static/src/xml/ceo_dashboard_v2.xml index db4af5e..bfe11f5 100644 --- a/addons/mcs_operating_system/static/src/xml/ceo_dashboard_v2.xml +++ b/addons/mcs_operating_system/static/src/xml/ceo_dashboard_v2.xml @@ -77,15 +77,18 @@