diff --git a/bemade_purchase_warn_supplier_overdue/models/res_company.py b/bemade_purchase_warn_supplier_overdue/models/res_company.py
index f993649..cb42e84 100644
--- a/bemade_purchase_warn_supplier_overdue/models/res_company.py
+++ b/bemade_purchase_warn_supplier_overdue/models/res_company.py
@@ -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(
diff --git a/customer_itch_cycle/models/itch_cycle_product_partner.py b/customer_itch_cycle/models/itch_cycle_product_partner.py
index 426d0a2..7dae52b 100644
--- a/customer_itch_cycle/models/itch_cycle_product_partner.py
+++ b/customer_itch_cycle/models/itch_cycle_product_partner.py
@@ -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'))
diff --git a/customer_itch_cycle/models/res_partner.py b/customer_itch_cycle/models/res_partner.py
index df98f40..e574881 100644
--- a/customer_itch_cycle/models/res_partner.py
+++ b/customer_itch_cycle/models/res_partner.py
@@ -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):
"""
diff --git a/customer_itch_cycle/models/sale_order_line.py b/customer_itch_cycle/models/sale_order_line.py
index 9ce7e98..55e3c22 100644
--- a/customer_itch_cycle/models/sale_order_line.py
+++ b/customer_itch_cycle/models/sale_order_line.py
@@ -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
\ No newline at end of file
+ @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
\ No newline at end of file
diff --git a/customer_itch_cycle/views/itch_cycle_product_partner_view.xml b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
index 17aab98..7bb46c0 100644
--- a/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
+++ b/customer_itch_cycle/views/itch_cycle_product_partner_view.xml
@@ -36,6 +36,12 @@
+
+
+
+
+
+
diff --git a/customer_itch_cycle/views/res_partner_view.xml b/customer_itch_cycle/views/res_partner_view.xml
index a233e96..5cd3564 100644
--- a/customer_itch_cycle/views/res_partner_view.xml
+++ b/customer_itch_cycle/views/res_partner_view.xml
@@ -13,15 +13,20 @@
class="btn-primary"/>
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odoo_unifi_manager/models/ctrl.py b/odoo_unifi_manager/models/ctrl.py
index b9a8d1d..921b629 100644
--- a/odoo_unifi_manager/models/ctrl.py
+++ b/odoo_unifi_manager/models/ctrl.py
@@ -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}")
\ No newline at end of file
+ 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}")
\ No newline at end of file
diff --git a/odoo_unifi_manager/models/firewall_rule.py b/odoo_unifi_manager/models/firewall_rule.py
index 6ea8984..845133f 100644
--- a/odoo_unifi_manager/models/firewall_rule.py
+++ b/odoo_unifi_manager/models/firewall_rule.py
@@ -9,7 +9,7 @@ class FirewallRule(models.Model):
required=True
)
- ctrl_id = fields.Many2one(
+ controller_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
diff --git a/odoo_unifi_manager/models/unifi_client.py b/odoo_unifi_manager/models/unifi_client.py
index 0c7d816..55ad5bd 100644
--- a/odoo_unifi_manager/models/unifi_client.py
+++ b/odoo_unifi_manager/models/unifi_client.py
@@ -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)}"
\ No newline at end of file
+ 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}")
\ No newline at end of file
diff --git a/odoo_unifi_manager/views/controller_views.xml b/odoo_unifi_manager/views/controller_views.xml
index 1ee6722..0dd2133 100644
--- a/odoo_unifi_manager/views/controller_views.xml
+++ b/odoo_unifi_manager/views/controller_views.xml
@@ -37,7 +37,9 @@
+
+