changes
This commit is contained in:
630
fusion_inventory/models/sync_config.py
Normal file
630
fusion_inventory/models/sync_config.py
Normal file
@@ -0,0 +1,630 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import logging
|
||||
import xmlrpc.client
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionSyncConfig(models.Model):
|
||||
_name = 'fusion.sync.config'
|
||||
_description = 'Inventory Sync Configuration'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(string='Connection Name', required=True)
|
||||
url = fields.Char(string='Remote URL', required=True,
|
||||
help='Full URL of the remote Odoo instance (e.g., https://erp.mobilityspecialties.com)')
|
||||
db_name = fields.Char(string='Database Name', required=True,
|
||||
help='Name of the remote database')
|
||||
username = fields.Char(string='Username', required=True,
|
||||
help='Login username for the remote instance')
|
||||
api_key = fields.Char(string='API Key / Password', required=True,
|
||||
help='API key or password for authentication')
|
||||
active = fields.Boolean(default=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Not Connected'),
|
||||
('connected', 'Connected'),
|
||||
('error', 'Connection Error'),
|
||||
], string='Status', default='draft', readonly=True)
|
||||
last_sync = fields.Datetime(string='Last Sync', readonly=True)
|
||||
last_sync_status = fields.Text(string='Last Sync Result', readonly=True)
|
||||
sync_interval = fields.Integer(string='Sync Interval (minutes)', default=30,
|
||||
help='How often to run the automatic sync')
|
||||
remote_uid = fields.Integer(string='Remote User ID', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
default=lambda self: self.env.company)
|
||||
|
||||
sync_products = fields.Boolean(string='Sync Products', default=True)
|
||||
sync_stock = fields.Boolean(string='Sync Stock Levels', default=True)
|
||||
remote_warehouse_name = fields.Char(
|
||||
string='Remote Warehouse Name',
|
||||
help='Name of the warehouse on the remote instance to read stock from. Leave empty for all.')
|
||||
|
||||
is_shared_warehouse = fields.Boolean(
|
||||
string='Shared Warehouse',
|
||||
help='Enable shared warehouse mode for cross-company inventory management')
|
||||
warehouse_location_id = fields.Many2one(
|
||||
'stock.location', string='Shared Warehouse Location',
|
||||
help='The stock location representing the shared warehouse')
|
||||
|
||||
remote_partner_id = fields.Many2one(
|
||||
'res.partner', string='Remote Company (Partner)',
|
||||
help='The local partner record that represents the remote company. '
|
||||
'Used as vendor when creating local POs for inter-company transfers.')
|
||||
local_company_name = fields.Char(
|
||||
string='This Company on Remote',
|
||||
help='The name of this company as it appears on the remote instance. '
|
||||
'Used to find the correct partner when creating SOs on the remote side.')
|
||||
|
||||
sync_warehouse_ids = fields.One2many(
|
||||
'fusion.sync.warehouse', 'config_id', string='Remote Warehouses')
|
||||
sync_warehouse_count = fields.Integer(
|
||||
compute='_compute_sync_warehouse_count')
|
||||
|
||||
@api.depends('sync_warehouse_ids')
|
||||
def _compute_sync_warehouse_count(self):
|
||||
for rec in self:
|
||||
rec.sync_warehouse_count = len(rec.sync_warehouse_ids)
|
||||
|
||||
# ── XML-RPC Connection ──
|
||||
|
||||
def _get_xmlrpc_connection(self):
|
||||
self.ensure_one()
|
||||
url = self.url.rstrip('/')
|
||||
try:
|
||||
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common', allow_none=True)
|
||||
uid = common.authenticate(self.db_name, self.username, self.api_key, {})
|
||||
if not uid:
|
||||
raise UserError('Authentication failed. Check username/API key.')
|
||||
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object', allow_none=True)
|
||||
return uid, models_proxy
|
||||
except xmlrpc.client.Fault as e:
|
||||
raise UserError(f'XML-RPC error: {e.faultString}')
|
||||
except Exception as e:
|
||||
raise UserError(f'Connection error: {str(e)}')
|
||||
|
||||
# ── Actions ──
|
||||
|
||||
def action_test_connection(self):
|
||||
self.ensure_one()
|
||||
try:
|
||||
uid, models_proxy = self._get_xmlrpc_connection()
|
||||
version_info = xmlrpc.client.ServerProxy(
|
||||
f'{self.url.rstrip("/")}/xmlrpc/2/common', allow_none=True
|
||||
).version()
|
||||
self.write({
|
||||
'state': 'connected',
|
||||
'remote_uid': uid,
|
||||
'last_sync_status': f'Connection successful. Remote server: {version_info.get("server_serie", "unknown")}',
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Connection Successful',
|
||||
'message': f'Connected to {self.url} as user ID {uid}',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'last_sync_status': f'Connection failed: {str(e)}',
|
||||
})
|
||||
raise
|
||||
|
||||
def action_sync_now(self):
|
||||
self.ensure_one()
|
||||
self._run_sync()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Sync Complete',
|
||||
'message': self.last_sync_status,
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
# ── Core Sync Logic ──
|
||||
|
||||
def _run_sync(self):
|
||||
self.ensure_one()
|
||||
try:
|
||||
uid, models_proxy = self._get_xmlrpc_connection()
|
||||
results = []
|
||||
|
||||
wh_count = self._sync_warehouses(uid, models_proxy)
|
||||
results.append(f'{wh_count} warehouses discovered')
|
||||
|
||||
if self.sync_products:
|
||||
count = self._sync_products(uid, models_proxy)
|
||||
results.append(f'{count} products synced')
|
||||
|
||||
if self.sync_stock:
|
||||
count = self._sync_stock_levels(uid, models_proxy)
|
||||
results.append(f'{count} stock levels updated')
|
||||
|
||||
status = ' | '.join(results)
|
||||
self.write({
|
||||
'state': 'connected',
|
||||
'last_sync': fields.Datetime.now(),
|
||||
'last_sync_status': status,
|
||||
})
|
||||
|
||||
self.env['fusion.sync.log'].create({
|
||||
'config_id': self.id,
|
||||
'direction': 'pull',
|
||||
'sync_type': 'full',
|
||||
'status': 'success',
|
||||
'summary': status,
|
||||
'product_count': sum(1 for r in results if 'product' in r.lower()),
|
||||
})
|
||||
_logger.info('Inventory sync complete for %s: %s', self.name, status)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'Sync failed: {str(e)}'
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'last_sync': fields.Datetime.now(),
|
||||
'last_sync_status': error_msg,
|
||||
})
|
||||
self.env['fusion.sync.log'].create({
|
||||
'config_id': self.id,
|
||||
'direction': 'pull',
|
||||
'sync_type': 'full',
|
||||
'status': 'error',
|
||||
'summary': error_msg,
|
||||
})
|
||||
_logger.error('Sync failed for %s: %s', self.name, error_msg)
|
||||
|
||||
def _sync_warehouses(self, uid, models_proxy):
|
||||
"""Discover and store remote warehouse metadata.
|
||||
Only syncs warehouses belonging to the remote user's main company."""
|
||||
self.ensure_one()
|
||||
SyncWH = self.env['fusion.sync.warehouse']
|
||||
|
||||
user_data = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'res.users', 'read', [[uid]],
|
||||
{'fields': ['company_id']}
|
||||
)
|
||||
wh_domain = []
|
||||
if user_data and user_data[0].get('company_id'):
|
||||
remote_company_id = user_data[0]['company_id']
|
||||
if isinstance(remote_company_id, (list, tuple)):
|
||||
remote_company_id = remote_company_id[0]
|
||||
wh_domain = [('company_id', '=', remote_company_id)]
|
||||
|
||||
remote_warehouses = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'stock.warehouse', 'search_read',
|
||||
[wh_domain],
|
||||
{'fields': ['id', 'name', 'code', 'company_id', 'lot_stock_id']}
|
||||
)
|
||||
|
||||
seen_ids = set()
|
||||
for rwh in remote_warehouses:
|
||||
wh_id = rwh['id']
|
||||
seen_ids.add(wh_id)
|
||||
company = rwh.get('company_id')
|
||||
company_name = company[1] if isinstance(company, (list, tuple)) else ''
|
||||
lot_stock = rwh.get('lot_stock_id')
|
||||
lot_stock_id = lot_stock[0] if isinstance(lot_stock, (list, tuple)) else (lot_stock or 0)
|
||||
|
||||
existing = SyncWH.search([
|
||||
('config_id', '=', self.id),
|
||||
('remote_warehouse_id', '=', wh_id),
|
||||
], limit=1)
|
||||
|
||||
vals = {
|
||||
'config_id': self.id,
|
||||
'remote_warehouse_id': wh_id,
|
||||
'remote_lot_stock_id': lot_stock_id,
|
||||
'name': rwh.get('name', ''),
|
||||
'code': rwh.get('code', ''),
|
||||
'company_name': company_name,
|
||||
}
|
||||
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
else:
|
||||
SyncWH.create(vals)
|
||||
|
||||
stale = SyncWH.search([
|
||||
('config_id', '=', self.id),
|
||||
('remote_warehouse_id', 'not in', list(seen_ids)),
|
||||
])
|
||||
if stale:
|
||||
stale.write({'active': False})
|
||||
|
||||
return len(remote_warehouses)
|
||||
|
||||
def _sync_products(self, uid, models_proxy):
|
||||
"""Pull remote product catalog and auto-match to local products."""
|
||||
self.ensure_one()
|
||||
Mapping = self.env['fusion.product.sync.mapping']
|
||||
|
||||
remote_products = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'product.template', 'search_read',
|
||||
[[('type', 'in', ['consu', 'product'])]],
|
||||
{'fields': ['id', 'name', 'default_code', 'barcode',
|
||||
'list_price', 'type', 'categ_id'],
|
||||
'limit': 10000}
|
||||
)
|
||||
|
||||
synced = 0
|
||||
for rp in remote_products:
|
||||
mapping = Mapping.search([
|
||||
('config_id', '=', self.id),
|
||||
('remote_product_id', '=', rp['id']),
|
||||
], limit=1)
|
||||
|
||||
vals = {
|
||||
'config_id': self.id,
|
||||
'remote_product_id': rp['id'],
|
||||
'remote_product_name': rp.get('name', ''),
|
||||
'remote_default_code': rp.get('default_code', '') or '',
|
||||
'remote_barcode': rp.get('barcode', '') or '',
|
||||
'remote_list_price': rp.get('list_price', 0),
|
||||
'remote_category': (
|
||||
rp['categ_id'][1]
|
||||
if isinstance(rp.get('categ_id'), (list, tuple))
|
||||
else ''),
|
||||
}
|
||||
|
||||
if mapping:
|
||||
mapping.write(vals)
|
||||
else:
|
||||
local_product = self._find_local_product(rp)
|
||||
vals['local_product_id'] = local_product.id if local_product else False
|
||||
vals['auto_matched'] = bool(local_product)
|
||||
Mapping.create(vals)
|
||||
|
||||
synced += 1
|
||||
|
||||
return synced
|
||||
|
||||
def _find_local_product(self, remote_product):
|
||||
"""Match a remote product to a local one by SKU, barcode, then name."""
|
||||
Template = self.env['product.template']
|
||||
code = remote_product.get('default_code')
|
||||
if code:
|
||||
match = Template.search([('default_code', '=', code)], limit=1)
|
||||
if match:
|
||||
return match
|
||||
|
||||
barcode = remote_product.get('barcode')
|
||||
if barcode:
|
||||
match = Template.search([('barcode', '=', barcode)], limit=1)
|
||||
if match:
|
||||
return match
|
||||
|
||||
name = remote_product.get('name')
|
||||
if name:
|
||||
match = Template.search([('name', '=ilike', name)], limit=1)
|
||||
if match:
|
||||
return match
|
||||
|
||||
return False
|
||||
|
||||
def _sync_stock_levels(self, uid, models_proxy):
|
||||
"""Pull per-warehouse stock levels and store in fusion.sync.stock."""
|
||||
self.ensure_one()
|
||||
Mapping = self.env['fusion.product.sync.mapping']
|
||||
SyncStock = self.env['fusion.sync.stock']
|
||||
|
||||
warehouses = self.sync_warehouse_ids.filtered('active')
|
||||
if not warehouses:
|
||||
_logger.info('No remote warehouses found for %s, skipping stock sync', self.name)
|
||||
return 0
|
||||
|
||||
mappings = Mapping.search([
|
||||
('config_id', '=', self.id),
|
||||
('remote_product_id', '!=', 0),
|
||||
])
|
||||
if not mappings:
|
||||
return 0
|
||||
|
||||
remote_tmpl_ids = [m.remote_product_id for m in mappings]
|
||||
mapping_by_remote = {m.remote_product_id: m for m in mappings}
|
||||
|
||||
total_updated = 0
|
||||
now = fields.Datetime.now()
|
||||
|
||||
for wh in warehouses:
|
||||
if not wh.remote_lot_stock_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
remote_quants = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'stock.quant', 'search_read',
|
||||
[[
|
||||
('location_id', 'child_of', wh.remote_lot_stock_id),
|
||||
('product_id.product_tmpl_id', 'in', remote_tmpl_ids),
|
||||
('quantity', '!=', 0),
|
||||
]],
|
||||
{'fields': ['product_id', 'quantity', 'reserved_quantity']}
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.warning(
|
||||
'Stock sync failed for warehouse %s: %s', wh.name, e)
|
||||
continue
|
||||
|
||||
stock_by_tmpl = {}
|
||||
product_tmpl_cache = {}
|
||||
|
||||
product_ids_needed = set()
|
||||
for q in remote_quants:
|
||||
pid = q['product_id']
|
||||
if isinstance(pid, (list, tuple)):
|
||||
pid = pid[0]
|
||||
product_ids_needed.add(pid)
|
||||
|
||||
if product_ids_needed:
|
||||
remote_variants = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'product.product', 'search_read',
|
||||
[[('id', 'in', list(product_ids_needed))]],
|
||||
{'fields': ['id', 'product_tmpl_id']}
|
||||
)
|
||||
for rv in remote_variants:
|
||||
tmpl = rv['product_tmpl_id']
|
||||
tmpl_id = tmpl[0] if isinstance(tmpl, (list, tuple)) else tmpl
|
||||
product_tmpl_cache[rv['id']] = tmpl_id
|
||||
|
||||
for q in remote_quants:
|
||||
pid = q['product_id']
|
||||
if isinstance(pid, (list, tuple)):
|
||||
pid = pid[0]
|
||||
tmpl_id = product_tmpl_cache.get(pid)
|
||||
if not tmpl_id:
|
||||
continue
|
||||
if tmpl_id not in stock_by_tmpl:
|
||||
stock_by_tmpl[tmpl_id] = {'qty': 0.0, 'reserved': 0.0}
|
||||
stock_by_tmpl[tmpl_id]['qty'] += q.get('quantity', 0)
|
||||
stock_by_tmpl[tmpl_id]['reserved'] += q.get('reserved_quantity', 0)
|
||||
|
||||
for tmpl_id, stock in stock_by_tmpl.items():
|
||||
mapping = mapping_by_remote.get(tmpl_id)
|
||||
if not mapping:
|
||||
continue
|
||||
|
||||
qty_available = stock['qty'] - stock['reserved']
|
||||
qty_forecast = stock['qty']
|
||||
|
||||
existing = SyncStock.search([
|
||||
('mapping_id', '=', mapping.id),
|
||||
('sync_warehouse_id', '=', wh.id),
|
||||
], limit=1)
|
||||
|
||||
vals = {
|
||||
'mapping_id': mapping.id,
|
||||
'sync_warehouse_id': wh.id,
|
||||
'qty_available': qty_available,
|
||||
'qty_forecast': qty_forecast,
|
||||
'last_sync': now,
|
||||
}
|
||||
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
else:
|
||||
SyncStock.create(vals)
|
||||
total_updated += 1
|
||||
|
||||
zero_mappings = set(mapping_by_remote.keys()) - set(stock_by_tmpl.keys())
|
||||
if zero_mappings:
|
||||
for tmpl_id in zero_mappings:
|
||||
mapping = mapping_by_remote.get(tmpl_id)
|
||||
if not mapping:
|
||||
continue
|
||||
existing = SyncStock.search([
|
||||
('mapping_id', '=', mapping.id),
|
||||
('sync_warehouse_id', '=', wh.id),
|
||||
], limit=1)
|
||||
if existing and existing.qty_available != 0:
|
||||
existing.write({
|
||||
'qty_available': 0,
|
||||
'qty_forecast': 0,
|
||||
'last_sync': now,
|
||||
})
|
||||
|
||||
for mapping in mappings:
|
||||
mapping.last_stock_sync = now
|
||||
|
||||
return total_updated
|
||||
|
||||
def _sync_stock_for_products(self, product_tmpl_ids):
|
||||
"""Targeted stock re-sync for specific products (called after stock moves)."""
|
||||
self.ensure_one()
|
||||
if not product_tmpl_ids:
|
||||
return
|
||||
|
||||
Mapping = self.env['fusion.product.sync.mapping']
|
||||
mappings = Mapping.search([
|
||||
('config_id', '=', self.id),
|
||||
('local_product_id', 'in', product_tmpl_ids),
|
||||
('remote_product_id', '!=', 0),
|
||||
])
|
||||
if not mappings:
|
||||
return
|
||||
|
||||
try:
|
||||
uid, models_proxy = self._get_xmlrpc_connection()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
warehouses = self.sync_warehouse_ids.filtered('active')
|
||||
SyncStock = self.env['fusion.sync.stock']
|
||||
now = fields.Datetime.now()
|
||||
|
||||
remote_tmpl_ids = [m.remote_product_id for m in mappings]
|
||||
mapping_by_remote = {m.remote_product_id: m for m in mappings}
|
||||
|
||||
for wh in warehouses:
|
||||
if not wh.remote_lot_stock_id:
|
||||
continue
|
||||
try:
|
||||
remote_quants = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'stock.quant', 'search_read',
|
||||
[[
|
||||
('location_id', 'child_of', wh.remote_lot_stock_id),
|
||||
('product_id.product_tmpl_id', 'in', remote_tmpl_ids),
|
||||
]],
|
||||
{'fields': ['product_id', 'quantity', 'reserved_quantity']}
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
stock_by_tmpl = {}
|
||||
product_ids_needed = set()
|
||||
for q in remote_quants:
|
||||
pid = q['product_id']
|
||||
if isinstance(pid, (list, tuple)):
|
||||
pid = pid[0]
|
||||
product_ids_needed.add(pid)
|
||||
|
||||
product_tmpl_cache = {}
|
||||
if product_ids_needed:
|
||||
remote_variants = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'product.product', 'search_read',
|
||||
[[('id', 'in', list(product_ids_needed))]],
|
||||
{'fields': ['id', 'product_tmpl_id']}
|
||||
)
|
||||
for rv in remote_variants:
|
||||
tmpl = rv['product_tmpl_id']
|
||||
product_tmpl_cache[rv['id']] = (
|
||||
tmpl[0] if isinstance(tmpl, (list, tuple)) else tmpl)
|
||||
|
||||
for q in remote_quants:
|
||||
pid = q['product_id']
|
||||
if isinstance(pid, (list, tuple)):
|
||||
pid = pid[0]
|
||||
tmpl_id = product_tmpl_cache.get(pid)
|
||||
if not tmpl_id:
|
||||
continue
|
||||
if tmpl_id not in stock_by_tmpl:
|
||||
stock_by_tmpl[tmpl_id] = {'qty': 0.0, 'reserved': 0.0}
|
||||
stock_by_tmpl[tmpl_id]['qty'] += q.get('quantity', 0)
|
||||
stock_by_tmpl[tmpl_id]['reserved'] += q.get('reserved_quantity', 0)
|
||||
|
||||
for tmpl_id in remote_tmpl_ids:
|
||||
mapping = mapping_by_remote.get(tmpl_id)
|
||||
if not mapping:
|
||||
continue
|
||||
stock = stock_by_tmpl.get(tmpl_id, {'qty': 0, 'reserved': 0})
|
||||
existing = SyncStock.search([
|
||||
('mapping_id', '=', mapping.id),
|
||||
('sync_warehouse_id', '=', wh.id),
|
||||
], limit=1)
|
||||
vals = {
|
||||
'mapping_id': mapping.id,
|
||||
'sync_warehouse_id': wh.id,
|
||||
'qty_available': stock['qty'] - stock['reserved'],
|
||||
'qty_forecast': stock['qty'],
|
||||
'last_sync': now,
|
||||
}
|
||||
if existing:
|
||||
existing.write(vals)
|
||||
else:
|
||||
SyncStock.create(vals)
|
||||
|
||||
# ── Inter-Company Transfer Helpers ──
|
||||
|
||||
def _create_remote_sale_order(self, product_mapping, qty, partner_name):
|
||||
"""Create a sale order on the remote instance for inter-company transfers."""
|
||||
self.ensure_one()
|
||||
uid, models_proxy = self._get_xmlrpc_connection()
|
||||
|
||||
partners = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'res.partner', 'search_read',
|
||||
[[('name', 'ilike', partner_name)]],
|
||||
{'fields': ['id', 'name'], 'limit': 1}
|
||||
)
|
||||
if not partners:
|
||||
raise UserError(f'Partner "{partner_name}" not found on remote instance.')
|
||||
|
||||
remote_product_ids = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'product.product', 'search',
|
||||
[[('product_tmpl_id', '=', product_mapping.remote_product_id)]],
|
||||
{'limit': 1}
|
||||
)
|
||||
if not remote_product_ids:
|
||||
raise UserError('Remote product variant not found.')
|
||||
|
||||
so_vals = {
|
||||
'partner_id': partners[0]['id'],
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': remote_product_ids[0],
|
||||
'product_uom_qty': qty,
|
||||
})],
|
||||
}
|
||||
|
||||
remote_so_id = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'sale.order', 'create', [so_vals]
|
||||
)
|
||||
|
||||
models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'sale.order', 'action_confirm', [[remote_so_id]]
|
||||
)
|
||||
|
||||
so_data = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'sale.order', 'read', [[remote_so_id]], {'fields': ['name']}
|
||||
)
|
||||
so_name = so_data[0]['name'] if so_data else ''
|
||||
|
||||
return remote_so_id, so_name
|
||||
|
||||
def _create_remote_invoice(self, remote_so_id):
|
||||
"""Create and post an invoice for a remote sale order."""
|
||||
self.ensure_one()
|
||||
uid, models_proxy = self._get_xmlrpc_connection()
|
||||
|
||||
models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'sale.order', 'action_create_invoices', [[remote_so_id]]
|
||||
)
|
||||
|
||||
so_data = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'sale.order', 'read', [[remote_so_id]],
|
||||
{'fields': ['invoice_ids']}
|
||||
)
|
||||
invoice_ids = so_data[0].get('invoice_ids', []) if so_data else []
|
||||
|
||||
if invoice_ids:
|
||||
inv_data = models_proxy.execute_kw(
|
||||
self.db_name, uid, self.api_key,
|
||||
'account.move', 'read',
|
||||
[invoice_ids],
|
||||
{'fields': ['id', 'name', 'amount_total']}
|
||||
)
|
||||
return inv_data[0]['id'] if inv_data else False
|
||||
|
||||
return False
|
||||
|
||||
# ── Cron ──
|
||||
|
||||
@api.model
|
||||
def _cron_sync_inventory(self):
|
||||
configs = self.search([('active', '=', True), ('state', '!=', 'draft')])
|
||||
for config in configs:
|
||||
try:
|
||||
config._run_sync()
|
||||
except Exception as e:
|
||||
_logger.error('Cron sync failed for %s: %s', config.name, e)
|
||||
Reference in New Issue
Block a user