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"),
|
||||
],
|
||||
default="all",
|
||||
help=(
|
||||
"Choose whether to apply overdue warnings to all vendors or only to "
|
||||
"specific vendors."
|
||||
),
|
||||
help="Choose whether to apply overdue warnings to all vendors or only to specific vendors.",
|
||||
)
|
||||
|
||||
warn_supplier_specific_ids = fields.Many2many(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,36 @@ class ItchCycleProductPartner(models.Model):
|
|||
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(
|
||||
string="Cycle moyen (jours)",
|
||||
compute="_compute_average_cycle",
|
||||
|
|
@ -142,7 +172,10 @@ class ItchCycleProductPartner(models.Model):
|
|||
"""
|
||||
# Récupérer toutes les lignes de commandes confirmées
|
||||
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)
|
||||
|
|
@ -193,3 +226,23 @@ class ItchCycleProductPartner(models.Model):
|
|||
'sale_order_line_ids': [(6, 0, data['sale_order_line_ids'])], # Lier ou actualiser les lignes
|
||||
})
|
||||
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"
|
||||
)
|
||||
|
||||
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(
|
||||
string="Prochaine Date de Suivi (Itch-Cycle Min)",
|
||||
compute="_compute_itch_next_delay",
|
||||
# compute="_compute_itch_next_delay",
|
||||
store=True
|
||||
)
|
||||
|
||||
|
|
@ -23,8 +30,11 @@ class ResPartner(models.Model):
|
|||
Calcule la date de suivi minimale parmi tous les cycles de produits associés.
|
||||
"""
|
||||
for partner in self:
|
||||
follow_up_dates = partner.itch_cycle_product_ids.mapped('next_follow_up_date')
|
||||
partner.itch_next_delay = min(follow_up_dates) if follow_up_dates else False
|
||||
follow_up_dates = partner.reorder_cycle_product_ids.mapped('next_follow_up_date')
|
||||
if follow_up_dates:
|
||||
partner.itch_next_delay = min(follow_up_dates)
|
||||
else:
|
||||
partner.itch_next_delay = False
|
||||
|
||||
def action_populate_itch_cycles(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -8,20 +8,24 @@ class SaleOrderLine(models.Model):
|
|||
string="Cycle Produit/Partenaire"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
line = super(SaleOrderLine, self).create(vals)
|
||||
# Associer au bon cycle produit/partenaire
|
||||
if vals.get('product_id') and vals.get('order_id'):
|
||||
partner_id = self.env['sale.order'].browse(vals['order_id']).partner_id.id
|
||||
itch_cycle = self.env['itch.cycle.product.partner'].search([
|
||||
('partner_id', '=', partner_id),
|
||||
('product_id', '=', vals['product_id'])
|
||||
], limit=1)
|
||||
if not itch_cycle:
|
||||
itch_cycle = self.env['itch.cycle.product.partner'].create({
|
||||
'partner_id': partner_id,
|
||||
'product_id': vals['product_id'],
|
||||
})
|
||||
line.itch_cycle_id = itch_cycle.id
|
||||
return line
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# Appeler le super pour créer les lignes
|
||||
lines = super().create(vals_list)
|
||||
|
||||
# Associer les lignes nouvellement créées avec leur cycle
|
||||
for line in lines:
|
||||
if line.product_id and line.order_id:
|
||||
partner_id = line.order_id.partner_id.id
|
||||
itch_cycle = self.env['itch.cycle.product.partner'].search([
|
||||
('partner_id', '=', partner_id),
|
||||
('product_id', '=', line.product_id.id)
|
||||
], limit=1)
|
||||
if not itch_cycle:
|
||||
itch_cycle = self.env['itch.cycle.product.partner'].create({
|
||||
'partner_id': partner_id,
|
||||
'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="next_expected_date" readonly="1"/>
|
||||
<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>
|
||||
<notebook>
|
||||
|
|
|
|||
|
|
@ -13,15 +13,20 @@
|
|||
class="btn-primary"/>
|
||||
<group string="Itch Cycle Information">
|
||||
<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>
|
||||
<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>
|
||||
</notebook>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from .unifi_client import Unifi
|
||||
|
||||
from odoo import models, fields, api
|
||||
from .unifi_client import Unifi
|
||||
from odoo.exceptions import ValidationError
|
||||
#from unificontrol import UnifiClient
|
||||
|
||||
|
||||
class UnifiCtrl(models.Model):
|
||||
|
|
@ -38,10 +36,26 @@ class UnifiCtrl(models.Model):
|
|||
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):
|
||||
"""Test connection to Unifi Controller."""
|
||||
"""Test connection to Unifi Controller or UDM Pro."""
|
||||
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:
|
||||
client.login()
|
||||
return {
|
||||
|
|
@ -54,4 +68,46 @@ class UnifiCtrl(models.Model):
|
|||
},
|
||||
}
|
||||
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
|
||||
)
|
||||
|
||||
ctrl_id = fields.Many2one(
|
||||
controller_id = fields.Many2one(
|
||||
'unifi.ctrl',
|
||||
string='Controller',
|
||||
required=True,
|
||||
|
|
|
|||
|
|
@ -2,24 +2,37 @@ import requests
|
|||
import urllib3
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
# API Endpoints
|
||||
UNIFI_LOGIN_PATH = '/api/auth/login'
|
||||
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()
|
||||
|
||||
|
||||
class Unifi:
|
||||
def __init__(self, host, username, password):
|
||||
def __init__(self, host, username, password, is_udm=False):
|
||||
"""Initialize Unifi client."""
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.is_udm = is_udm
|
||||
self.session = requests.Session()
|
||||
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):
|
||||
"""Authenticate with the Unifi Controller."""
|
||||
"""Authenticate with the Unifi Controller or UDM Pro."""
|
||||
payload = {
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
|
|
@ -37,7 +50,7 @@ class Unifi:
|
|||
"""Send a request to the Unifi API."""
|
||||
if data is None:
|
||||
data = {}
|
||||
uri = f'https://{self.host}{path}'
|
||||
uri = self._get_url(path)
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
|
|
@ -63,11 +76,35 @@ class Unifi:
|
|||
except requests.RequestException as e:
|
||||
raise ValidationError(f"Failed to retrieve sites: {e}")
|
||||
|
||||
def test_connection(self):
|
||||
"""Test connection and fetch available sites."""
|
||||
if not self.login():
|
||||
raise ValidationError("Login failed. Check your credentials.")
|
||||
def get_firewall_rules(self):
|
||||
"""Retrieve all firewall rules."""
|
||||
try:
|
||||
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()
|
||||
site_list = [site['desc'] for site in sites]
|
||||
return f"Connection successful! Available sites: {', '.join(site_list)}"
|
||||
def get_networks(self):
|
||||
"""Retrieve all networks."""
|
||||
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="username"/>
|
||||
<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_fetch_networks_and_fill_rules" type="object" string="Fetch Networks and Create Rules" class="btn-secondary"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
|
|
|||
Loading…
Reference in a new issue