diff --git a/addons/mcs_operating_system/hooks.py b/addons/mcs_operating_system/hooks.py index 41a8786..b7ea46b 100644 --- a/addons/mcs_operating_system/hooks.py +++ b/addons/mcs_operating_system/hooks.py @@ -38,8 +38,7 @@ 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 + values.update(self._client_company_currency_values(partner)) partner.write(values) def _ensure_active_portfolio(self): @@ -143,12 +142,47 @@ 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 + values.update(self._client_company_currency_values(partner)) if values: partner.write(values) return partner + def _client_company_currency_values(self, partner): + invoice = self.Invoice.search( + [ + ("move_type", "=", "out_invoice"), + ("state", "=", "posted"), + ("commercial_partner_id", "=", partner.commercial_partner_id.id), + ("company_id", "!=", False), + ], + order="invoice_date desc, id desc", + limit=1, + ) + if invoice: + return { + "mcs_client_company_id": invoice.company_id.id, + "mcs_currency_id": invoice.currency_id.id or invoice.company_currency_id.id, + } + project = self.Project.search( + [("partner_id", "=", partner.id), ("company_id", "!=", False)], + order="write_date desc, id desc", + limit=1, + ) + if project: + return { + "mcs_client_company_id": project.company_id.id, + "mcs_currency_id": project.company_id.currency_id.id, + } + if partner.currency_id: + company = self.env["res.company"].sudo().search( + [("currency_id", "=", partner.currency_id.id)], limit=1 + ) + values = {"mcs_currency_id": partner.currency_id.id} + if company: + values["mcs_client_company_id"] = company.id + return values + return {} + def _ensure_project(self, partner, account_name, workstream, delivery_type): is_internal = partner.name == "Internal" project_name = "%s - %s" % (partner.name, workstream) diff --git a/addons/mcs_operating_system/models/crm_lead.py b/addons/mcs_operating_system/models/crm_lead.py index f928848..cebda6c 100644 --- a/addons/mcs_operating_system/models/crm_lead.py +++ b/addons/mcs_operating_system/models/crm_lead.py @@ -107,6 +107,10 @@ class CrmLead(models.Model): partner = lead.partner_id if partner: values = {"mcs_is_client_account": True} + company = lead.company_id or self.env.company + if company: + values["mcs_client_company_id"] = company.id + values["mcs_currency_id"] = company.currency_id.id if not partner.mcs_account_owner_id and lead.user_id: values["mcs_account_owner_id"] = lead.user_id.id partner.write(values) @@ -123,6 +127,7 @@ class CrmLead(models.Model): { "name": project_name, "partner_id": partner.id if partner else False, + "company_id": lead.company_id.id if lead.company_id else self.env.company.id, "user_id": lead.user_id.id if lead.user_id else False, "allow_timesheets": True, "allow_milestones": True, diff --git a/addons/mcs_operating_system/models/res_partner.py b/addons/mcs_operating_system/models/res_partner.py index 7131e55..fcf531c 100644 --- a/addons/mcs_operating_system/models/res_partner.py +++ b/addons/mcs_operating_system/models/res_partner.py @@ -35,6 +35,11 @@ class ResPartner(models.Model): string="MCS Currency", default=lambda self: self.env.company.currency_id, ) + mcs_client_company_id = fields.Many2one( + "res.company", + string="MCS Company", + help="Company inferred from this client's invoices or delivery projects.", + ) mcs_monthly_revenue = fields.Monetary( string="Monthly Revenue", currency_field="mcs_currency_id", @@ -63,16 +68,28 @@ class ResPartner(models.Model): currency_field="mcs_currency_id", compute="_compute_mcs_account_metrics", ) + mcs_total_invoiced_display = fields.Char( + string="Total Invoiced", + 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_outstanding_display = fields.Char( + string="Outstanding Receivables", + compute="_compute_mcs_account_metrics", + ) mcs_total_overdue = fields.Monetary( string="Overdue Receivables", currency_field="mcs_currency_id", compute="_compute_mcs_account_metrics", ) + mcs_total_overdue_display = fields.Char( + string="Overdue Receivables", + compute="_compute_mcs_account_metrics", + ) mcs_last_invoice_date = fields.Date( string="Last Invoice Date", compute="_compute_mcs_account_metrics", @@ -100,13 +117,17 @@ class ResPartner(models.Model): "mcs_account_status": "varchar", "mcs_account_health": "varchar", "mcs_currency_id": "integer", + "mcs_client_company_id": "integer", "mcs_monthly_revenue": "numeric", "mcs_contracted_value": "numeric", "mcs_monthly_hours": "double precision", "mcs_effective_hourly_revenue": "numeric", "mcs_total_invoiced": "numeric", + "mcs_total_invoiced_display": "varchar", "mcs_total_outstanding": "numeric", + "mcs_total_outstanding_display": "varchar", "mcs_total_overdue": "numeric", + "mcs_total_overdue_display": "varchar", "mcs_last_invoice_date": "date", "mcs_account_owner_id": "integer", "mcs_next_account_action": "varchar", @@ -137,7 +158,8 @@ class ResPartner(models.Model): ("move_type", "=", "out_invoice"), ("state", "=", "posted"), ("commercial_partner_id", "=", commercial_partner.id), - ] + ], + order="invoice_date desc, id desc", ) overdue_invoices = invoices.filtered( lambda invoice: invoice.invoice_date_due @@ -145,10 +167,10 @@ class ResPartner(models.Model): 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 - ) + account_company = partner._mcs_resolve_client_company(invoices, projects) + account_currency = partner._mcs_resolve_client_currency(invoices, projects, account_company) + partner.mcs_client_company_id = account_company.id if account_company else False + partner.mcs_currency_id = account_currency.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")) @@ -158,18 +180,70 @@ class ResPartner(models.Model): else 0 ) partner.mcs_total_invoiced = sum(invoices.mapped("amount_total_signed")) + partner.mcs_total_invoiced_display = partner._mcs_invoice_amount_display( + invoices, "amount_total", account_currency + ) 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_outstanding_display = partner._mcs_invoice_amount_display( + invoices.filtered( + lambda invoice: invoice.payment_state + not in ("paid", "in_payment", "reversed") + ), + "amount_residual", + account_currency, + ) partner.mcs_total_overdue = sum(overdue_invoices.mapped("amount_residual_signed")) + partner.mcs_total_overdue_display = partner._mcs_invoice_amount_display( + overdue_invoices, "amount_residual", account_currency + ) 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) ) + def _mcs_resolve_client_company(self, invoices, projects): + company = invoices[:1].company_id if invoices else self.env["res.company"] + if not company and projects: + company = projects[:1].company_id + if not company: + company = self.company_id + return company + + def _mcs_resolve_client_currency(self, invoices, projects, company): + currencies = invoices.mapped("currency_id") + if len(currencies) == 1: + return currencies + if invoices: + return invoices[:1].currency_id or invoices[:1].company_currency_id + if projects and projects[:1].company_id: + return projects[:1].company_id.currency_id + if company: + return company.currency_id + return self.currency_id or self.env.company.currency_id + + def _mcs_invoice_amount_display(self, invoices, amount_field, default_currency): + buckets = {} + for invoice in invoices: + currency = invoice.currency_id or invoice.company_currency_id or default_currency + buckets[currency.id] = buckets.get(currency.id, 0) + (invoice[amount_field] or 0) + if not buckets and default_currency: + buckets[default_currency.id] = 0 + if not buckets: + buckets[self.env.company.currency_id.id] = 0 + Currency = self.env["res.currency"].sudo() + return " / ".join( + "%s %s" % (currency.name, "{:,.2f}".format(round(amount or 0, 2))) + for currency, amount in sorted( + ((Currency.browse(currency_id), amount) for currency_id, amount in buckets.items()), + key=lambda item: item[0].name, + ) + ) + def _mcs_get_account_health_signals(self, projects): """Return health signals used to resolve client account health. diff --git a/addons/mcs_operating_system/views/res_partner_views.xml b/addons/mcs_operating_system/views/res_partner_views.xml index 16645f9..96b136b 100644 --- a/addons/mcs_operating_system/views/res_partner_views.xml +++ b/addons/mcs_operating_system/views/res_partner_views.xml @@ -9,6 +9,7 @@ + @@ -19,9 +20,9 @@ - - - + + + @@ -50,15 +51,16 @@ + - - - + + + @@ -66,12 +68,33 @@ + + res.partner.search.mcs.client.account + res.partner + + + + + + + + + + + + + + + + + Client Accounts res.partner tree,form + [('mcs_is_client_account','=',True)] - {'default_mcs_is_client_account': True} + {'default_mcs_is_client_account': True, 'group_by': 'mcs_client_company_id'}