itch et unifi
This commit is contained in:
parent
9578594628
commit
536f10b61e
10 changed files with 221 additions and 51 deletions
|
|
@ -33,10 +33,7 @@ class Company(models.Model):
|
||||||
("specific", "Specific Vendors"),
|
("specific", "Specific Vendors"),
|
||||||
],
|
],
|
||||||
default="all",
|
default="all",
|
||||||
help=(
|
help="Choose whether to apply overdue warnings to all vendors or only to specific vendors.",
|
||||||
"Choose whether to apply overdue warnings to all vendors or only to "
|
|
||||||
"specific vendors."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
warn_supplier_specific_ids = fields.Many2many(
|
warn_supplier_specific_ids = fields.Many2many(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,36 @@ class ItchCycleProductPartner(models.Model):
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
quantity_total_ordered = fields.Float(
|
||||||
|
string="Quantité totale commandée",
|
||||||
|
compute="_compute_quantity_total_ordered",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity_of_orders = fields.Integer(
|
||||||
|
string="Nombre de commandes",
|
||||||
|
compute="_compute_quantity_of_orders",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
min_quantity_ordered = fields.Float(
|
||||||
|
string="Quantité minimale commandée",
|
||||||
|
compute="_compute_min_quantity_ordered",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
max_quantity_ordered = fields.Float(
|
||||||
|
string="Quantité maximale commandée",
|
||||||
|
compute="_compute_max_quantity_ordered",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mean_quantity_ordered = fields.Float(
|
||||||
|
string="Quantité moyenne commandée",
|
||||||
|
compute="_compute_mean_quantity_ordered",
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
|
||||||
average_cycle = fields.Integer(
|
average_cycle = fields.Integer(
|
||||||
string="Cycle moyen (jours)",
|
string="Cycle moyen (jours)",
|
||||||
compute="_compute_average_cycle",
|
compute="_compute_average_cycle",
|
||||||
|
|
@ -142,7 +172,10 @@ class ItchCycleProductPartner(models.Model):
|
||||||
"""
|
"""
|
||||||
# Récupérer toutes les lignes de commandes confirmées
|
# Récupérer toutes les lignes de commandes confirmées
|
||||||
sale_order_lines = self.env['sale.order.line'].search([
|
sale_order_lines = self.env['sale.order.line'].search([
|
||||||
('order_id.state', 'in', ['sale', 'done']) # Commandes confirmées ou terminées
|
('order_id.state', 'in', ['sale', 'done']), # Commandes confirmées ou terminées
|
||||||
|
('qty_delivered', '>', 0), # Quantité livrée supérieure à 0
|
||||||
|
('product_id', '!=', False), # Exclure les lignes sans produit
|
||||||
|
('order_id.partner_id', '=', 5024659), # un client pour test
|
||||||
])
|
])
|
||||||
|
|
||||||
# Carte pour regrouper par couple (client, produit)
|
# Carte pour regrouper par couple (client, produit)
|
||||||
|
|
@ -193,3 +226,23 @@ class ItchCycleProductPartner(models.Model):
|
||||||
'sale_order_line_ids': [(6, 0, data['sale_order_line_ids'])], # Lier ou actualiser les lignes
|
'sale_order_line_ids': [(6, 0, data['sale_order_line_ids'])], # Lier ou actualiser les lignes
|
||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _compute_quantity_total_ordered(self):
|
||||||
|
for record in self:
|
||||||
|
record.quantity_total_ordered = sum(record.sale_order_line_ids.mapped('product_uom_qty'))
|
||||||
|
|
||||||
|
def _compute_quantity_of_orders(self):
|
||||||
|
for record in self:
|
||||||
|
record.quantity_of_orders = len(record.sale_order_line_ids)
|
||||||
|
|
||||||
|
def _compute_min_quantity_ordered(self):
|
||||||
|
for record in self:
|
||||||
|
record.min_quantity_ordered = min(record.sale_order_line_ids.mapped('product_uom_qty'))
|
||||||
|
|
||||||
|
def _compute_max_quantity_ordered(self):
|
||||||
|
for record in self:
|
||||||
|
record.max_quantity_ordered = max(record.sale_order_line_ids.mapped('product_uom_qty'))
|
||||||
|
|
||||||
|
def _compute_mean_quantity_ordered(self):
|
||||||
|
for record in self:
|
||||||
|
record.mean_quantity_ordered = np.mean(record.sale_order_line_ids.mapped('product_uom_qty'))
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,16 @@ class ResPartner(models.Model):
|
||||||
string="Itch Cycles Produits"
|
string="Itch Cycles Produits"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
reorder_cycle_product_ids = fields.One2many(
|
||||||
|
comodel_name='itch.cycle.product.partner',
|
||||||
|
inverse_name='partner_id',
|
||||||
|
string="Itch Cycles Produits",
|
||||||
|
domain=[('quantity_of_orders', '>', 1)]
|
||||||
|
)
|
||||||
|
|
||||||
itch_next_delay = fields.Date(
|
itch_next_delay = fields.Date(
|
||||||
string="Prochaine Date de Suivi (Itch-Cycle Min)",
|
string="Prochaine Date de Suivi (Itch-Cycle Min)",
|
||||||
compute="_compute_itch_next_delay",
|
# compute="_compute_itch_next_delay",
|
||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,8 +30,11 @@ class ResPartner(models.Model):
|
||||||
Calcule la date de suivi minimale parmi tous les cycles de produits associés.
|
Calcule la date de suivi minimale parmi tous les cycles de produits associés.
|
||||||
"""
|
"""
|
||||||
for partner in self:
|
for partner in self:
|
||||||
follow_up_dates = partner.itch_cycle_product_ids.mapped('next_follow_up_date')
|
follow_up_dates = partner.reorder_cycle_product_ids.mapped('next_follow_up_date')
|
||||||
partner.itch_next_delay = min(follow_up_dates) if follow_up_dates else False
|
if follow_up_dates:
|
||||||
|
partner.itch_next_delay = min(follow_up_dates)
|
||||||
|
else:
|
||||||
|
partner.itch_next_delay = False
|
||||||
|
|
||||||
def action_populate_itch_cycles(self):
|
def action_populate_itch_cycles(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,24 @@ class SaleOrderLine(models.Model):
|
||||||
string="Cycle Produit/Partenaire"
|
string="Cycle Produit/Partenaire"
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.model
|
@api.model_create_multi
|
||||||
def create(self, vals):
|
def create(self, vals_list):
|
||||||
line = super(SaleOrderLine, self).create(vals)
|
# Appeler le super pour créer les lignes
|
||||||
# Associer au bon cycle produit/partenaire
|
lines = super().create(vals_list)
|
||||||
if vals.get('product_id') and vals.get('order_id'):
|
|
||||||
partner_id = self.env['sale.order'].browse(vals['order_id']).partner_id.id
|
# Associer les lignes nouvellement créées avec leur cycle
|
||||||
itch_cycle = self.env['itch.cycle.product.partner'].search([
|
for line in lines:
|
||||||
('partner_id', '=', partner_id),
|
if line.product_id and line.order_id:
|
||||||
('product_id', '=', vals['product_id'])
|
partner_id = line.order_id.partner_id.id
|
||||||
], limit=1)
|
itch_cycle = self.env['itch.cycle.product.partner'].search([
|
||||||
if not itch_cycle:
|
('partner_id', '=', partner_id),
|
||||||
itch_cycle = self.env['itch.cycle.product.partner'].create({
|
('product_id', '=', line.product_id.id)
|
||||||
'partner_id': partner_id,
|
], limit=1)
|
||||||
'product_id': vals['product_id'],
|
if not itch_cycle:
|
||||||
})
|
itch_cycle = self.env['itch.cycle.product.partner'].create({
|
||||||
line.itch_cycle_id = itch_cycle.id
|
'partner_id': partner_id,
|
||||||
return line
|
'product_id': line.product_id.id,
|
||||||
|
})
|
||||||
|
line.itch_cycle_id = itch_cycle.id
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
@ -36,6 +36,12 @@
|
||||||
<field name="last_purchase_date" readonly="1"/>
|
<field name="last_purchase_date" readonly="1"/>
|
||||||
<field name="next_expected_date" readonly="1"/>
|
<field name="next_expected_date" readonly="1"/>
|
||||||
<field name="next_follow_up_date"/>
|
<field name="next_follow_up_date"/>
|
||||||
|
<field name="prediction_status" widget="statusbar" options="{'on_time': 'green', 'delayed': 'red'}"/>
|
||||||
|
<field name="quantity_of_orders" readonly="1"/>
|
||||||
|
<field name="quantity_total_ordered" readonly="1"/>
|
||||||
|
<field name="min_quantity_ordered" readonly="1"/>
|
||||||
|
<field name="max_quantity_ordered" readonly="1"/>
|
||||||
|
<field name="mean_quantity_ordered" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,20 @@
|
||||||
class="btn-primary"/>
|
class="btn-primary"/>
|
||||||
<group string="Itch Cycle Information">
|
<group string="Itch Cycle Information">
|
||||||
<field name="itch_next_delay" readonly="1"/>
|
<field name="itch_next_delay" readonly="1"/>
|
||||||
<field name="itch_cycle_product_ids" readonly="1">
|
|
||||||
<tree>
|
|
||||||
<field name="product_id"/>
|
|
||||||
<field name="last_purchase_date"/>
|
|
||||||
<field name="itch_cycle_duration"/>
|
|
||||||
<field name="next_follow_up_date"/>
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</group>
|
</group>
|
||||||
|
<field name="reorder_cycle_product_ids" readonly="1">
|
||||||
|
<tree>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="last_purchase_date"/>
|
||||||
|
<field name="itch_cycle_duration"/>
|
||||||
|
<field name="next_follow_up_date"/>
|
||||||
|
<field name="min_quantity_ordered"/>
|
||||||
|
<field name="mean_quantity_ordered"/>
|
||||||
|
<field name="max_quantity_ordered"/>
|
||||||
|
<field name="quantity_of_orders"/>
|
||||||
|
<field name="quantity_total_ordered"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
from .unifi_client import Unifi
|
|
||||||
|
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
|
from .unifi_client import Unifi
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
#from unificontrol import UnifiClient
|
|
||||||
|
|
||||||
|
|
||||||
class UnifiCtrl(models.Model):
|
class UnifiCtrl(models.Model):
|
||||||
|
|
@ -38,10 +36,26 @@ class UnifiCtrl(models.Model):
|
||||||
help="Password for API authentication."
|
help="Password for API authentication."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
controller_type = fields.Selection(
|
||||||
|
[
|
||||||
|
('udm', 'UDM Pro'),
|
||||||
|
('controller', 'Controller')
|
||||||
|
],
|
||||||
|
string="Controller Type",
|
||||||
|
default='udm',
|
||||||
|
required=True,
|
||||||
|
help="Select whether this is a UDM Pro or a Unifi Controller."
|
||||||
|
)
|
||||||
|
|
||||||
def action_test_connection(self):
|
def action_test_connection(self):
|
||||||
"""Test connection to Unifi Controller."""
|
"""Test connection to Unifi Controller or UDM Pro."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
client = Unifi(self.ip_address, self.username, self.password)
|
client = Unifi(
|
||||||
|
host=self.ip_address,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
is_udm=(self.controller_type == 'udm')
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
client.login()
|
client.login()
|
||||||
return {
|
return {
|
||||||
|
|
@ -54,4 +68,46 @@ class UnifiCtrl(models.Model):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(f"Connection failed: {e}")
|
raise ValidationError(f"Connection failed: {e}")
|
||||||
|
|
||||||
|
def action_fetch_networks_and_fill_rules(self):
|
||||||
|
"""Fetch networks and populate firewall rules."""
|
||||||
|
self.ensure_one()
|
||||||
|
client = Unifi(
|
||||||
|
host=self.ip_address,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
is_udm=(self.controller_type == 'udm')
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
client.login()
|
||||||
|
networks = client.get_networks()
|
||||||
|
|
||||||
|
firewall_rule_model = self.env['firewall.rule']
|
||||||
|
for network in networks:
|
||||||
|
rule_name = network.get('name', 'Unnamed Rule')
|
||||||
|
src_ip = network.get('subnet', None) # Example: Adjust based on API data
|
||||||
|
if not src_ip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
firewall_rule_model.create({
|
||||||
|
'name': f"Network: {rule_name}",
|
||||||
|
'action': 'accept',
|
||||||
|
'enabled': True,
|
||||||
|
'src_ip': src_ip,
|
||||||
|
'dst_ip': None,
|
||||||
|
'log': False,
|
||||||
|
'controller_id': self.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': 'Networks Fetched',
|
||||||
|
'message': f"{len(networks)} networks fetched and rules created.",
|
||||||
|
'type': 'success',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError(f"Failed to fetch networks: {e}")
|
||||||
|
|
@ -9,7 +9,7 @@ class FirewallRule(models.Model):
|
||||||
required=True
|
required=True
|
||||||
)
|
)
|
||||||
|
|
||||||
ctrl_id = fields.Many2one(
|
controller_id = fields.Many2one(
|
||||||
'unifi.ctrl',
|
'unifi.ctrl',
|
||||||
string='Controller',
|
string='Controller',
|
||||||
required=True,
|
required=True,
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,37 @@ import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
# API Endpoints
|
||||||
UNIFI_LOGIN_PATH = '/api/auth/login'
|
UNIFI_LOGIN_PATH = '/api/auth/login'
|
||||||
UNIFI_SITES_PATH = '/api/self/sites'
|
UNIFI_SITES_PATH = '/api/self/sites'
|
||||||
|
UNIFI_FIREWALL_RULES_PATH = '/api/s/default/rest/firewallrule'
|
||||||
|
UNIFI_NETWORKS_PATH = '/api/s/default/rest/network'
|
||||||
|
UNIFI_WIFIS_PATH = '/api/s/default/rest/wlanconf'
|
||||||
|
|
||||||
# Désactiver les avertissements SSL pour certificats auto-signés
|
# Prefix for UDM Pro endpoints
|
||||||
|
UDM_PROXY_PREFIX = '/proxy/network'
|
||||||
|
|
||||||
|
# Disable SSL verification warnings
|
||||||
urllib3.disable_warnings()
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
|
||||||
class Unifi:
|
class Unifi:
|
||||||
def __init__(self, host, username, password):
|
def __init__(self, host, username, password, is_udm=False):
|
||||||
"""Initialize Unifi client."""
|
"""Initialize Unifi client."""
|
||||||
self.host = host
|
self.host = host
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.is_udm = is_udm
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.csrf = ""
|
self.csrf = ""
|
||||||
|
|
||||||
|
def _get_url(self, path):
|
||||||
|
"""Construct the correct URL based on the type of controller."""
|
||||||
|
prefix = UDM_PROXY_PREFIX if self.is_udm else ""
|
||||||
|
return f"https://{self.host}{prefix}{path}"
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
"""Authenticate with the Unifi Controller."""
|
"""Authenticate with the Unifi Controller or UDM Pro."""
|
||||||
payload = {
|
payload = {
|
||||||
"username": self.username,
|
"username": self.username,
|
||||||
"password": self.password,
|
"password": self.password,
|
||||||
|
|
@ -37,7 +50,7 @@ class Unifi:
|
||||||
"""Send a request to the Unifi API."""
|
"""Send a request to the Unifi API."""
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {}
|
data = {}
|
||||||
uri = f'https://{self.host}{path}'
|
uri = self._get_url(path)
|
||||||
headers = {
|
headers = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
|
@ -63,11 +76,35 @@ class Unifi:
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
raise ValidationError(f"Failed to retrieve sites: {e}")
|
raise ValidationError(f"Failed to retrieve sites: {e}")
|
||||||
|
|
||||||
def test_connection(self):
|
def get_firewall_rules(self):
|
||||||
"""Test connection and fetch available sites."""
|
"""Retrieve all firewall rules."""
|
||||||
if not self.login():
|
try:
|
||||||
raise ValidationError("Login failed. Check your credentials.")
|
r = self.request(UNIFI_FIREWALL_RULES_PATH, method='GET')
|
||||||
|
if r.ok:
|
||||||
|
return r.json().get('data', [])
|
||||||
|
else:
|
||||||
|
raise ValidationError(f"Failed to retrieve firewall rules: {r.text}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise ValidationError(f"Request to fetch firewall rules failed: {e}")
|
||||||
|
|
||||||
sites = self.get_sites()
|
def get_networks(self):
|
||||||
site_list = [site['desc'] for site in sites]
|
"""Retrieve all networks."""
|
||||||
return f"Connection successful! Available sites: {', '.join(site_list)}"
|
try:
|
||||||
|
r = self.request(UNIFI_NETWORKS_PATH, method='GET')
|
||||||
|
if r.ok:
|
||||||
|
return r.json().get('data', [])
|
||||||
|
else:
|
||||||
|
raise ValidationError(f"Failed to retrieve networks: {r.text}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise ValidationError(f"Request to fetch networks failed: {e}")
|
||||||
|
|
||||||
|
def get_wifis(self):
|
||||||
|
"""Retrieve all WiFi configurations."""
|
||||||
|
try:
|
||||||
|
r = self.request(UNIFI_WIFIS_PATH, method='GET')
|
||||||
|
if r.ok:
|
||||||
|
return r.json().get('data', [])
|
||||||
|
else:
|
||||||
|
raise ValidationError(f"Failed to retrieve WiFi configurations: {r.text}")
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise ValidationError(f"Request to fetch WiFi configurations failed: {e}")
|
||||||
|
|
@ -37,7 +37,9 @@
|
||||||
<field name="api_port"/>
|
<field name="api_port"/>
|
||||||
<field name="username"/>
|
<field name="username"/>
|
||||||
<field name="password" password="True"/>
|
<field name="password" password="True"/>
|
||||||
|
<field name="controller_type"/>
|
||||||
<button name="action_test_connection" type="object" string="Test Connection" class="btn-primary"/>
|
<button name="action_test_connection" type="object" string="Test Connection" class="btn-primary"/>
|
||||||
|
<button name="action_fetch_networks_and_fill_rules" type="object" string="Fetch Networks and Create Rules" class="btn-secondary"/>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue