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 @@ +