-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ADD] engenere_partner_sales_info: Initial commit
- Loading branch information
1 parent
fed39e4
commit 4436f8d
Showing
11 changed files
with
379 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from . import res_partner | ||
from . import res_config_settings |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
134
engenere_partner_sales_info/tests/test_partner_sales_info.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
27
engenere_partner_sales_info/views/res_config_settings_views.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
1 change: 1 addition & 0 deletions
1
setup/engenere_partner_sales_info/odoo/addons/engenere_partner_sales_info
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../engenere_partner_sales_info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |