Map operating accounts to existing customers
This commit is contained in:
parent
84b23c268e
commit
7859d6a7a1
@ -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": [
|
||||
"ClicksToCart": {
|
||||
"aliases": ["Clickstocart Inc", "ClicksToCart"],
|
||||
"workstreams": [
|
||||
("ERP Implementation", "fixed_project"),
|
||||
("App Development", "fixed_project"),
|
||||
("Social Media Handling", "recurring"),
|
||||
],
|
||||
"Wallaceton": [
|
||||
},
|
||||
"Wallaceton": {
|
||||
"aliases": ["Wallaceton Eye Care", "Wallaceton"],
|
||||
"workstreams": [
|
||||
("SEO", "recurring"),
|
||||
("Social Media Handling", "recurring"),
|
||||
("Ads Management", "recurring"),
|
||||
],
|
||||
"Maison de Treats": [("Social Media Handling", "recurring")],
|
||||
"Lens and Frames": [
|
||||
},
|
||||
"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": [
|
||||
},
|
||||
"TNCSC": {
|
||||
"aliases": ["Tamil Nadu Cultural Society of Canada", "TNCSC"],
|
||||
"workstreams": [
|
||||
("Ads Management", "recurring"),
|
||||
("Videos", "fixed_project"),
|
||||
("ERP", "fixed_project"),
|
||||
],
|
||||
"Infini": [("Social Media Handling", "recurring")],
|
||||
"Organic Healthy Family": [("Shopify Store Development", "fixed_project")],
|
||||
"Hondavert": [
|
||||
},
|
||||
"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": [("Website Development", "fixed_project")],
|
||||
"Racewerks": [("Website Development", "fixed_project")],
|
||||
"Internal": [
|
||||
},
|
||||
"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})
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -77,15 +77,18 @@
|
||||
</header>
|
||||
<div class="o_mcs_table">
|
||||
<div class="o_mcs_table_head">
|
||||
<span>Client</span><span>Health</span><span>Streams</span><span>Revenue</span><span>Hours</span><span>Next Action</span>
|
||||
<span>Client</span><span>Health</span><span>Currency</span><span>Streams</span><span>Invoiced</span><span>Outstanding</span><span>Overdue</span><span>Last Invoice</span><span>Next Action</span>
|
||||
</div>
|
||||
<t t-foreach="state.data.portfolio" t-as="row" t-key="row.id">
|
||||
<button class="o_mcs_table_row" t-on-click="() => this.openClient(row.id)">
|
||||
<span t-esc="row.name"/>
|
||||
<span t-attf-class="o_mcs_badge o_mcs_status_{{ row.health }}" t-esc="row.health"/>
|
||||
<span t-esc="row.currency"/>
|
||||
<span t-esc="row.active_workstreams"/>
|
||||
<span t-esc="row.revenue"/>
|
||||
<span t-esc="row.hours"/>
|
||||
<span t-esc="row.total_invoiced"/>
|
||||
<span t-esc="row.outstanding"/>
|
||||
<span t-esc="row.overdue"/>
|
||||
<span t-esc="row.last_invoice_date"/>
|
||||
<span t-esc="row.next_action"/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
@ -35,6 +35,12 @@ class TestCeoDashboardV2(TransactionCase):
|
||||
self.assertEqual(self.partner.mcs_monthly_revenue, 100000)
|
||||
self.assertEqual(self.partner.mcs_contracted_value, 80000)
|
||||
|
||||
def test_client_account_invoice_rollup_fields_exist(self):
|
||||
self.assertIn("mcs_total_invoiced", self.partner._fields)
|
||||
self.assertIn("mcs_total_outstanding", self.partner._fields)
|
||||
self.assertIn("mcs_total_overdue", self.partner._fields)
|
||||
self.assertIn("mcs_last_invoice_date", self.partner._fields)
|
||||
|
||||
def test_account_health_uses_extensible_signals(self):
|
||||
signals = self.partner._mcs_get_account_health_signals(self.project)
|
||||
self.assertIn("project_health_values", signals)
|
||||
|
||||
@ -14,11 +14,15 @@
|
||||
<field name="mcs_account_owner_id"/>
|
||||
</group>
|
||||
<group string="Commercial Rollup">
|
||||
<field name="mcs_currency_id" invisible="1"/>
|
||||
<field name="mcs_currency_id"/>
|
||||
<field name="mcs_monthly_revenue"/>
|
||||
<field name="mcs_contracted_value"/>
|
||||
<field name="mcs_monthly_hours"/>
|
||||
<field name="mcs_effective_hourly_revenue"/>
|
||||
<field name="mcs_total_invoiced"/>
|
||||
<field name="mcs_total_outstanding"/>
|
||||
<field name="mcs_total_overdue"/>
|
||||
<field name="mcs_last_invoice_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
@ -52,6 +56,10 @@
|
||||
<field name="mcs_currency_id" column_invisible="True"/>
|
||||
<field name="mcs_monthly_revenue"/>
|
||||
<field name="mcs_contracted_value"/>
|
||||
<field name="mcs_total_invoiced"/>
|
||||
<field name="mcs_total_outstanding"/>
|
||||
<field name="mcs_total_overdue"/>
|
||||
<field name="mcs_last_invoice_date"/>
|
||||
<field name="mcs_monthly_hours"/>
|
||||
<field name="mcs_next_account_action"/>
|
||||
</tree>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user