Skip to content

Commit

Permalink
[ADD] engenere_partner_sales_info: Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
felipemotter committed Feb 6, 2025
1 parent fed39e4 commit 4436f8d
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 0 deletions.
1 change: 1 addition & 0 deletions engenere_partner_sales_info/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
17 changes: 17 additions & 0 deletions engenere_partner_sales_info/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Partner Sales Information",
"version": "14.0.1.0.0",
"category": "Sales",
"summary": "Add sales analysis fields to partners",
"author": "Engenere",
"maintainers": ["felipempereira"],
"website": "https://engenere.one",
"depends": ["sale", "account"],
"data": [
"views/res_partner_views.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,
"license": "AGPL-3",
}
2 changes: 2 additions & 0 deletions engenere_partner_sales_info/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import res_partner
from . import res_config_settings
11 changes: 11 additions & 0 deletions engenere_partner_sales_info/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

partner_sales_info_months = fields.Integer(
string="Analysis Period (Months)",
default=24,
config_parameter="engenere_partner_sales_info.default_analysis_months",
)
150 changes: 150 additions & 0 deletions engenere_partner_sales_info/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import math
from collections import defaultdict

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models


class ResPartner(models.Model):
_inherit = "res.partner"

last_order_date = fields.Date(
string="Last Order Date", compute="_compute_sales_info"
)
last_order_status = fields.Char(
string="Last Order Status", compute="_compute_sales_info"
)
last_invoice_date = fields.Date(
string="Last Invoice Date", compute="_compute_sales_info"
)
invoice_count = fields.Integer(
string="Number of Invoices", compute="_compute_sales_info"
)
total_invoiced = fields.Monetary(
string="Total Invoiced", compute="_compute_sales_info"
)
average_invoiced = fields.Monetary(
string="Average Invoiced", compute="_compute_sales_info"
)
average_invoiced_no_discrepancies = fields.Monetary(
string="Average Invoiced (No Discrepancies)", compute="_compute_sales_info"
)
average_time_between_invoices = fields.Float(
string="Average Time Between Invoices (Days)", compute="_compute_sales_info"
)
last_invoice_id = fields.Many2one(
"account.move", string="Last Invoice", compute="_compute_sales_info"
)

@api.depends()
def _compute_sales_info(self):
config_param = self.env["ir.config_parameter"].sudo()
analysis_months = int(
config_param.get_param(
"engenere_partner_sales_info.default_analysis_months", 24
)
)
start_date = fields.Date.context_today(self) - relativedelta(
months=analysis_months
)
partners = self.filtered(lambda p: p.customer_rank > 0)
partner_ids = partners.ids

sale_orders = self.env["sale.order"].search(
[
("partner_id", "in", partner_ids),
("state", "!=", "cancel"),
("date_order", ">=", start_date),
]
)
from_so = defaultdict(list)
for so in sale_orders:
from_so[so.partner_id.id].append(so)

invoices = self.env["account.move"].search(
[
("partner_id", "in", partner_ids),
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("reversed_entry_id", "=", False),
("invoice_date", ">=", start_date),
]
)
from_inv = defaultdict(list)
for inv in invoices:
from_inv[inv.partner_id.id].append(inv)

for partner in partners:
so_list = sorted(from_so.get(partner.id, []), key=lambda x: x.date_order)
if so_list:
partner.last_order_date = so_list[-1].date_order.date()
partner.last_order_status = so_list[-1].state
else:
partner.last_order_date = False
partner.last_order_status = False

inv_list = sorted(
from_inv.get(partner.id, []), key=lambda x: x.invoice_date
)
if inv_list:
partner.last_invoice_date = inv_list[-1].invoice_date
partner.last_invoice_id = inv_list[-1].id
count = len(inv_list)
total = sum(i.amount_total for i in inv_list)
avg = total / count if count else 0

if count >= 3:
amounts = [inv.amount_total for inv in inv_list]
mean = total / count
variance = sum((x - mean) ** 2 for x in amounts) / count
std_dev = math.sqrt(variance)
lower_bound = mean - 1.5 * std_dev # Adjust multiplier as needed
upper_bound = mean + 1.5 * std_dev
filtered_amounts = [
x for x in amounts if lower_bound <= x <= upper_bound
]

if filtered_amounts:
avg_no_disc = sum(filtered_amounts) / len(filtered_amounts)
else:
avg_no_disc = mean # Fallback to original mean
else:
avg_no_disc = avg

avg_time = 0
if count >= 2:
dates = [i.invoice_date for i in inv_list]
total_days = 0
for i in range(1, len(dates)):
total_days += (dates[i] - dates[i - 1]).days
avg_time = total_days / (len(dates) - 1)

partner.invoice_count = count
partner.total_invoiced = total
partner.average_invoiced = avg
partner.average_invoiced_no_discrepancies = avg_no_disc
partner.average_time_between_invoices = avg_time
else:
partner.last_invoice_date = False
partner.last_invoice_id = False
partner.invoice_count = 0
partner.total_invoiced = 0
partner.average_invoiced = 0
partner.average_invoiced_no_discrepancies = 0
partner.average_time_between_invoices = 0

for partner in self - partners:
partner.update(
{
"last_order_date": False,
"last_order_status": False,
"last_invoice_date": False,
"invoice_count": 0,
"total_invoiced": 0,
"average_invoiced": 0,
"average_invoiced_no_discrepancies": 0,
"average_time_between_invoices": 0,
"last_invoice_id": False,
}
)
1 change: 1 addition & 0 deletions engenere_partner_sales_info/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_partner_sales_info
134 changes: 134 additions & 0 deletions engenere_partner_sales_info/tests/test_partner_sales_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# test_partner_sales_info.py
# -*- coding: utf-8 -*-
from dateutil.relativedelta import relativedelta

from odoo import fields
from odoo.tests import common


class TestPartnerSalesInfo(common.TransactionCase):
def setUp(self):
super().setUp()
self.partner_model = self.env["res.partner"]
self.sale_model = self.env["sale.order"]
self.invoice_model = self.env["account.move"]
self.config_param = self.env["ir.config_parameter"].sudo()

# Criar contas contábeis necessárias
self.account_receivable = self.env["account.account"].create(
{
"name": "Test Receivable Account",
"code": "TREC",
"user_type_id": self.env.ref("account.data_account_type_receivable").id,
"reconcile": True,
"company_id": self.env.company.id,
}
)

self.account_income = self.env["account.account"].create(
{
"name": "Test Income Account",
"code": "TIN",
"user_type_id": self.env.ref("account.data_account_type_revenue").id,
"company_id": self.env.company.id,
}
)

# Configurar diário de vendas
self.sale_journal = self.env["account.journal"].create(
{
"name": "Test Sale Journal",
"type": "sale",
"code": "TSJ",
"company_id": self.env.company.id,
"default_account_id": self.account_income.id,
}
)

# Configurar parceiro com conta a receber
self.customer_partner = self.partner_model.create(
{
"name": "Test Customer",
"customer_rank": 1,
"property_account_receivable_id": self.account_receivable.id,
}
)

# Configurar parâmetro de meses de análise
self.config_param.set_param(
"engenere_partner_sales_info.default_analysis_months", 12
)

def test_no_sales_no_invoices(self):
self.customer_partner._compute_sales_info()
self.assertFalse(self.customer_partner.last_order_date)
self.assertFalse(self.customer_partner.last_order_status)
self.assertFalse(self.customer_partner.last_invoice_date)
self.assertEqual(self.customer_partner.invoice_count, 0)
self.assertEqual(self.customer_partner.total_invoiced, 0)
self.assertEqual(self.customer_partner.average_invoiced, 0)
self.assertEqual(self.customer_partner.average_invoiced_no_discrepancies, 0)
self.assertEqual(self.customer_partner.average_time_between_invoices, 0)
self.assertFalse(self.customer_partner.last_invoice_id)

def test_with_sales(self):
order_vals = {
"partner_id": self.customer_partner.id,
"date_order": fields.Datetime.now() - relativedelta(days=2),
"state": "sale",
}
so = self.sale_model.create(order_vals)
self.customer_partner._compute_sales_info()
self.assertEqual(self.customer_partner.last_order_date, so.date_order.date())
self.assertEqual(self.customer_partner.last_order_status, so.state)

def test_with_invoices(self):
self._create_invoice(self.customer_partner, 100, days_diff=10)
inv = self._create_invoice(self.customer_partner, 200, days_diff=5)
self.customer_partner._compute_sales_info()
self.assertEqual(self.customer_partner.invoice_count, 2)
self.assertAlmostEqual(self.customer_partner.total_invoiced, 300)
self.assertAlmostEqual(self.customer_partner.average_invoiced, 150)
self.assertEqual(self.customer_partner.last_invoice_id, inv)
self.assertEqual(self.customer_partner.last_invoice_date, inv.invoice_date)

def test_with_invoices_and_outliers(self):
self._create_invoice(self.customer_partner, 100, days_diff=12)
self._create_invoice(self.customer_partner, 110, days_diff=8)
self._create_invoice(self.customer_partner, 120, days_diff=4)
self._create_invoice(self.customer_partner, 10000, days_diff=1)

self.customer_partner._compute_sales_info()
self.assertEqual(self.customer_partner.invoice_count, 4)
total_expected = 100 + 110 + 120 + 10000
self.assertAlmostEqual(self.customer_partner.total_invoiced, total_expected)
self.assertTrue(
self.customer_partner.average_invoiced > 100
and self.customer_partner.average_invoiced < total_expected
)
self.assertTrue(self.customer_partner.average_invoiced_no_discrepancies < 1000)

def _create_invoice(self, partner, amount, days_diff=0):
inv_date = fields.Date.today() - relativedelta(days=days_diff)
invoice = self.invoice_model.create(
{
"partner_id": partner.id,
"move_type": "out_invoice",
"invoice_date": inv_date,
"journal_id": self.sale_journal.id,
"invoice_line_ids": [
(
0,
0,
{
"name": "Test Line",
"quantity": 1,
"price_unit": amount,
"account_id": self.account_income.id,
},
)
],
}
)
invoice.action_post()
return invoice
27 changes: 27 additions & 0 deletions engenere_partner_sales_info/views/res_config_settings_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<odoo>
<record id="res_config_settings_view_form_inherit" model="ir.ui.view">
<field name="name">
res.config.settings.view.form.inherit.sales.info
</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<h2>Sales Analysis</h2>
<div class="row mt16">
<div class="col-12">
<div class="alert alert-info" role="alert">
Configure the default analysis period for partner
sales information.
</div>
</div>
<div class="col-12">
<group string="Analysis Settings">
<field name="partner_sales_info_months" />
</group>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
29 changes: 29 additions & 0 deletions engenere_partner_sales_info/views/res_partner_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<odoo>
<record id="view_partner_form_sales_info" model="ir.ui.view">
<field name="name">res.partner.form.sales.info</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//page[@name='internal_notes']" position="after">
<field name="customer_rank" invisible="True" />
<page
string="Sales Analysis"
name="partner_sales_analysis"
attrs="{'invisible': [('customer_rank', '=', 0)]}"
>
<group string="Sales Information">
<field name="last_order_date" />
<field name="last_order_status" />
<field name="last_invoice_date" />
<field name="invoice_count" />
<field name="total_invoiced" />
<field name="average_invoiced" />
<field name="average_invoiced_no_discrepancies" />
<field name="average_time_between_invoices" />
<field name="last_invoice_id" />
</group>
</page>
</xpath>
</field>
</record>
</odoo>
6 changes: 6 additions & 0 deletions setup/engenere_partner_sales_info/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

0 comments on commit 4436f8d

Please sign in to comment.