# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2013 Camptocamp SA
# Copyright 2013 Akretion
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from datetime import datetime, timedelta
from openerp import models, fields, api, _
from openerp.exceptions import Warning as UserError
from openerp.addons.connector.session import ConnectorSession
from openerp.addons.connector.connector import ConnectorUnit
from openerp.addons.connector.unit.mapper import mapping, ImportMapper
from .unit.backend_adapter import GenericAdapter
from .unit.import_synchronizer import (import_batch,
DirectBatchImporter,
MagentoImporter,
)
from .partner import partner_import_batch
from .sale import sale_order_import_batch
from .backend import magento
from .connector import add_checkpoint
_logger = logging.getLogger(__name__)
IMPORT_DELTA_BUFFER = 30 # seconds
[docs]class MagentoBackend(models.Model):
_name = 'magento.backend'
_description = 'Magento Backend'
_inherit = 'connector.backend'
_backend_type = 'magento'
[docs] @api.model
def select_versions(self):
""" Available versions in the backend.
Can be inherited to add custom versions. Using this method
to add a version from an ``_inherit`` does not constrain
to redefine the ``version`` field in the ``_inherit`` model.
"""
return [('1.7', '1.7+')]
@api.model
def _get_stock_field_id(self):
field = self.env['ir.model.fields'].search(
[('model', '=', 'product.product'),
('name', '=', 'virtual_available')],
limit=1)
return field
version = fields.Selection(selection='select_versions', required=True)
location = fields.Char(
string='Location',
required=True,
help="Url to magento application",
)
admin_location = fields.Char(string='Admin Location')
use_custom_api_path = fields.Boolean(
string='Custom Api Path',
help="The default API path is '/index.php/api/xmlrpc'. "
"Check this box if you use a custom API path, in that case, "
"the location has to be completed with the custom API path ",
)
username = fields.Char(
string='Username',
help="Webservice user",
)
password = fields.Char(
string='Password',
help="Webservice password",
)
use_auth_basic = fields.Boolean(
string='Use HTTP Auth Basic',
help="Use a Basic Access Authentication for the API. "
"The Magento server could be configured to restrict access "
"using a HTTP authentication based on a username and "
"a password.",
)
auth_basic_username = fields.Char(
string='Basic Auth. Username',
help="Basic access authentication web server side username",
)
auth_basic_password = fields.Char(
string='Basic Auth. Password',
help="Basic access authentication web server side password",
)
sale_prefix = fields.Char(
string='Sale Prefix',
help="A prefix put before the name of imported sales orders.\n"
"For instance, if the prefix is 'mag-', the sales "
"order 100000692 in Magento, will be named 'mag-100000692' "
"in OpenERP.",
)
warehouse_id = fields.Many2one(
comodel_name='stock.warehouse',
string='Warehouse',
required=True,
help='Warehouse used to compute the '
'stock quantities.',
)
company_id = fields.Many2one(
comodel_name='res.company',
related='warehouse_id.company_id',
string='Company',
readonly=True,
)
website_ids = fields.One2many(
comodel_name='magento.website',
inverse_name='backend_id',
string='Website',
readonly=True,
)
default_lang_id = fields.Many2one(
comodel_name='res.lang',
string='Default Language',
help="If a default language is selected, the records "
"will be imported in the translation of this language.\n"
"Note that a similar configuration exists "
"for each storeview.",
)
default_category_id = fields.Many2one(
comodel_name='product.category',
string='Default Product Category',
help='If a default category is selected, products imported '
'without a category will be linked to it.',
)
# TODO? add a field `auto_activate` -> activate a cron
import_products_from_date = fields.Datetime(
string='Import products from date',
)
import_categories_from_date = fields.Datetime(
string='Import categories from date',
)
product_stock_field_id = fields.Many2one(
comodel_name='ir.model.fields',
string='Stock Field',
default=_get_stock_field_id,
domain="[('model', 'in', ['product.product', 'product.template']),"
" ('ttype', '=', 'float')]",
help="Choose the field of the product which will be used for "
"stock inventory updates.\nIf empty, Quantity Available "
"is used.",
)
product_binding_ids = fields.One2many(
comodel_name='magento.product.product',
inverse_name='backend_id',
string='Magento Products',
readonly=True,
)
account_analytic_id = fields.Many2one(
comodel_name='account.analytic.account',
string='Analytic account',
help='If specified, this analytic account will be used to fill the '
'field on the sale order created by the connector. The value can '
'also be specified on website or the store or the store view.'
)
fiscal_position_id = fields.Many2one(
comodel_name='account.fiscal.position',
string='Fiscal position',
help='If specified, this fiscal position will be used to fill the '
'field fiscal position on the sale order created by the connector.'
'The value can also be specified on website or the store or the '
'store view.'
)
_sql_constraints = [
('sale_prefix_uniq', 'unique(sale_prefix)',
"A backend with the same sale prefix already exists")
]
[docs] @api.multi
def check_magento_structure(self):
""" Used in each data import.
Verify if a website exists for each backend before starting the import.
"""
for backend in self:
websites = backend.website_ids
if not websites:
backend.synchronize_metadata()
return True
[docs] @api.multi
def import_partners(self):
""" Import partners from all websites """
for backend in self:
backend.check_magento_structure()
backend.website_ids.import_partners()
return True
[docs] @api.multi
def import_sale_orders(self):
""" Import sale orders from all store views """
storeview_obj = self.env['magento.storeview']
storeviews = storeview_obj.search([('backend_id', 'in', self.ids)])
storeviews.import_sale_orders()
return True
[docs] @api.multi
def import_customer_groups(self):
session = ConnectorSession(self.env.cr, self.env.uid,
context=self.env.context)
for backend in self:
backend.check_magento_structure()
import_batch.delay(session, 'magento.res.partner.category',
backend.id)
return True
@api.multi
def _import_from_date(self, model, from_date_field):
session = ConnectorSession(self.env.cr, self.env.uid,
context=self.env.context)
import_start_time = datetime.now()
for backend in self:
backend.check_magento_structure()
from_date = getattr(backend, from_date_field)
if from_date:
from_date = fields.Datetime.from_string(from_date)
else:
from_date = None
import_batch.delay(session, model,
backend.id,
filters={'from_date': from_date,
'to_date': import_start_time})
# Records from Magento are imported based on their `created_at`
# date. This date is set on Magento at the beginning of a
# transaction, so if the import is run between the beginning and
# the end of a transaction, the import of a record may be
# missed. That's why we add a small buffer back in time where
# the eventually missed records will be retrieved. This also
# means that we'll have jobs that import twice the same records,
# but this is not a big deal because they will be skipped when
# the last `sync_date` is the same.
next_time = import_start_time - timedelta(seconds=IMPORT_DELTA_BUFFER)
next_time = fields.Datetime.to_string(next_time)
self.write({from_date_field: next_time})
[docs] @api.multi
def import_product_categories(self):
self._import_from_date('magento.product.category',
'import_categories_from_date')
return True
[docs] @api.multi
def import_product_product(self):
self._import_from_date('magento.product.product',
'import_products_from_date')
return True
@api.multi
def _domain_for_update_product_stock_qty(self):
return [
('backend_id', 'in', self.ids),
('type', '!=', 'service'),
('no_stock_sync', '=', False),
]
[docs] @api.multi
def update_product_stock_qty(self):
mag_product_obj = self.env['magento.product.product']
domain = self._domain_for_update_product_stock_qty()
magento_products = mag_product_obj.search(domain)
magento_products.recompute_magento_qty()
return True
@api.model
def _magento_backend(self, callback, domain=None):
if domain is None:
domain = []
backends = self.search(domain)
if backends:
getattr(backends, callback)()
@api.model
def _scheduler_import_sale_orders(self, domain=None):
self._magento_backend('import_sale_orders', domain=domain)
@api.model
def _scheduler_import_customer_groups(self, domain=None):
self._magento_backend('import_customer_groups', domain=domain)
@api.model
def _scheduler_import_partners(self, domain=None):
self._magento_backend('import_partners', domain=domain)
@api.model
def _scheduler_import_product_categories(self, domain=None):
self._magento_backend('import_product_categories', domain=domain)
@api.model
def _scheduler_import_product_product(self, domain=None):
self._magento_backend('import_product_product', domain=domain)
@api.model
def _scheduler_update_product_stock_qty(self, domain=None):
self._magento_backend('update_product_stock_qty', domain=domain)
[docs] @api.multi
def output_recorder(self):
""" Utility method to output a file containing all the recorded
requests / responses with Magento. Used to generate test data.
Should be called with ``erppeek`` for instance.
"""
from .unit.backend_adapter import output_recorder
import os
import tempfile
fmt = '%Y-%m-%d-%H-%M-%S'
timestamp = datetime.now().strftime(fmt)
filename = 'output_%s_%s' % (self.env.cr.dbname, timestamp)
path = os.path.join(tempfile.gettempdir(), filename)
output_recorder(path)
return path
[docs]class MagentoConfigSpecializer(models.AbstractModel):
_name = 'magento.config.specializer'
specific_account_analytic_id = fields.Many2one(
comodel_name='account.analytic.account',
string='Specific analytic account',
help='If specified, this analytic account will be used to fill the '
'field on the sale order created by the connector. The value can '
'also be specified on website or the store or the store view.'
)
specific_fiscal_position_id = fields.Many2one(
comodel_name='account.fiscal.position',
string='Specific fiscal position',
help='If specified, this fiscal position will be used to fill the '
'field fiscal position on the sale order created by the connector.'
'The value can also be specified on website or the store or the '
'store view.'
)
account_analytic_id = fields.Many2one(
comodel_name='account.analytic.account',
string='Analytic account',
compute='_get_account_analytic_id',
)
fiscal_position_id = fields.Many2one(
comodel_name='account.fiscal.position',
string='Fiscal position',
compute='_get_fiscal_position_id',
)
@property
def _parent(self):
return getattr(self, self._parent_name)
@api.multi
def _get_account_analytic_id(self):
for this in self:
this.account_analytic_id = (
this.specific_account_analytic_id or
this._parent.account_analytic_id)
@api.multi
def _get_fiscal_position_id(self):
for this in self:
this.fiscal_position_id = (
this.specific_fiscal_position_id or
this._parent.fiscal_position_id)
[docs]class MagentoWebsite(models.Model):
_name = 'magento.website'
_inherit = ['magento.binding', 'magento.config.specializer']
_description = 'Magento Website'
_parent_name = 'backend_id'
_order = 'sort_order ASC, id ASC'
name = fields.Char(required=True, readonly=True)
code = fields.Char(readonly=True)
sort_order = fields.Integer(string='Sort Order', readonly=True)
store_ids = fields.One2many(
comodel_name='magento.store',
inverse_name='website_id',
string='Stores',
readonly=True,
)
import_partners_from_date = fields.Datetime(
string='Import partners from date',
)
product_binding_ids = fields.Many2many(
comodel_name='magento.product.product',
string='Magento Products',
readonly=True,
)
[docs] @api.multi
def import_partners(self):
session = ConnectorSession(self.env.cr, self.env.uid,
context=self.env.context)
import_start_time = datetime.now()
for website in self:
backend_id = website.backend_id.id
if website.import_partners_from_date:
from_string = fields.Datetime.from_string
from_date = from_string(website.import_partners_from_date)
else:
from_date = None
partner_import_batch.delay(
session, 'magento.res.partner', backend_id,
{'magento_website_id': website.magento_id,
'from_date': from_date,
'to_date': import_start_time})
# Records from Magento are imported based on their `created_at`
# date. This date is set on Magento at the beginning of a
# transaction, so if the import is run between the beginning and
# the end of a transaction, the import of a record may be
# missed. That's why we add a small buffer back in time where
# the eventually missed records will be retrieved. This also
# means that we'll have jobs that import twice the same records,
# but this is not a big deal because they will be skipped when
# the last `sync_date` is the same.
next_time = import_start_time - timedelta(seconds=IMPORT_DELTA_BUFFER)
next_time = fields.Datetime.to_string(next_time)
self.write({'import_partners_from_date': next_time})
return True
[docs]class MagentoStore(models.Model):
_name = 'magento.store'
_inherit = ['magento.binding', 'magento.config.specializer']
_description = 'Magento Store'
_parent_name = 'website_id'
name = fields.Char()
website_id = fields.Many2one(
comodel_name='magento.website',
string='Magento Website',
required=True,
readonly=True,
ondelete='cascade',
)
backend_id = fields.Many2one(
comodel_name='magento.backend',
related='website_id.backend_id',
string='Magento Backend',
store=True,
readonly=True,
# override 'magento.binding', can't be INSERTed if True:
required=False,
)
storeview_ids = fields.One2many(
comodel_name='magento.storeview',
inverse_name='store_id',
string="Storeviews",
readonly=True,
)
send_picking_done_mail = fields.Boolean(
string='Send email notification on picking done',
help="Does the picking export/creation should send "
"an email notification on Magento side?",
)
send_invoice_paid_mail = fields.Boolean(
string='Send email notification on invoice validated/paid',
help="Does the invoice export/creation should send "
"an email notification on Magento side?",
)
create_invoice_on = fields.Selection(
selection=[('open', 'Validate'),
('paid', 'Paid')],
string='Create invoice on action',
default='paid',
required=True,
help="Should the invoice be created in Magento "
"when it is validated or when it is paid in OpenERP?\n"
"This only takes effect if the sales order's related "
"payment method is not giving an option for this by "
"itself. (See Payment Methods)",
)
[docs]class MagentoStoreview(models.Model):
_name = 'magento.storeview'
_inherit = ['magento.binding', 'magento.config.specializer']
_description = "Magento Storeview"
_parent_name = 'store_id'
_order = 'sort_order ASC, id ASC'
name = fields.Char(required=True, readonly=True)
code = fields.Char(readonly=True)
enabled = fields.Boolean(string='Enabled', readonly=True)
sort_order = fields.Integer(string='Sort Order', readonly=True)
store_id = fields.Many2one(comodel_name='magento.store',
string='Store',
ondelete='cascade',
readonly=True)
lang_id = fields.Many2one(comodel_name='res.lang', string='Language')
section_id = fields.Many2one(comodel_name='crm.case.section',
string='Sales Team')
backend_id = fields.Many2one(
comodel_name='magento.backend',
related='store_id.website_id.backend_id',
string='Magento Backend',
store=True,
readonly=True,
# override 'magento.binding', can't be INSERTed if True:
required=False,
)
import_orders_from_date = fields.Datetime(
string='Import sale orders from date',
help='do not consider non-imported sale orders before this date. '
'Leave empty to import all sale orders',
)
no_sales_order_sync = fields.Boolean(
string='No Sales Order Synchronization',
help='Check if the storeview is active in Magento '
'but its sales orders should not be imported.',
)
catalog_price_tax_included = fields.Boolean(string='Prices include tax')
[docs] @api.multi
def import_sale_orders(self):
session = ConnectorSession(self.env.cr, self.env.uid,
context=self.env.context)
import_start_time = datetime.now()
for storeview in self:
if storeview.no_sales_order_sync:
_logger.debug("The storeview '%s' is active in Magento "
"but is configured not to import the "
"sales orders", storeview.name)
continue
backend_id = storeview.backend_id.id
if storeview.import_orders_from_date:
from_string = fields.Datetime.from_string
from_date = from_string(storeview.import_orders_from_date)
else:
from_date = None
sale_order_import_batch.delay(
session,
'magento.sale.order',
backend_id,
{'magento_storeview_id': storeview.magento_id,
'from_date': from_date,
'to_date': import_start_time},
priority=1) # executed as soon as possible
# Records from Magento are imported based on their `created_at`
# date. This date is set on Magento at the beginning of a
# transaction, so if the import is run between the beginning and
# the end of a transaction, the import of a record may be
# missed. That's why we add a small buffer back in time where
# the eventually missed records will be retrieved. This also
# means that we'll have jobs that import twice the same records,
# but this is not a big deal because the sales orders will be
# imported the first time and the jobs will be skipped on the
# subsequent imports
next_time = import_start_time - timedelta(seconds=IMPORT_DELTA_BUFFER)
next_time = fields.Datetime.to_string(next_time)
self.write({'import_orders_from_date': next_time})
return True
[docs]@magento
class WebsiteAdapter(GenericAdapter):
_model_name = 'magento.website'
_magento_model = 'ol_websites'
_admin_path = 'system_store/editWebsite/website_id/{id}'
[docs]@magento
class StoreAdapter(GenericAdapter):
_model_name = 'magento.store'
_magento_model = 'ol_groups'
_admin_path = 'system_store/editGroup/group_id/{id}'
[docs]@magento
class StoreviewAdapter(GenericAdapter):
_model_name = 'magento.storeview'
_magento_model = 'ol_storeviews'
_admin_path = 'system_store/editStore/store_id/{id}'
MetadataBatchImport = MetadataBatchImporter # deprecated
[docs]@magento
class WebsiteImportMapper(ImportMapper):
_model_name = 'magento.website'
direct = [('code', 'code'),
('sort_order', 'sort_order')]
[docs] @mapping
def name(self, record):
name = record['name']
if name is None:
name = _('Undefined')
return {'name': name}
[docs] @mapping
def backend_id(self, record):
return {'backend_id': self.backend_record.id}
[docs]@magento
class StoreImportMapper(ImportMapper):
_model_name = 'magento.store'
direct = [('name', 'name')]
[docs] @mapping
def website_id(self, record):
binder = self.binder_for(model='magento.website')
binding_id = binder.to_openerp(record['website_id'])
return {'website_id': binding_id}
[docs]@magento
class StoreviewImportMapper(ImportMapper):
_model_name = 'magento.storeview'
direct = [
('name', 'name'),
('code', 'code'),
('is_active', 'enabled'),
('sort_order', 'sort_order'),
]
[docs] @mapping
def store_id(self, record):
binder = self.binder_for(model='magento.store')
binding_id = binder.to_openerp(record['group_id'])
return {'store_id': binding_id}
[docs]@magento
class StoreImporter(MagentoImporter):
""" Import one Magento Store (create a sale.shop via _inherits) """
_model_name = ['magento.store',
]
def _create(self, data):
binding = super(StoreImporter, self)._create(data)
checkpoint = self.unit_for(StoreAddCheckpoint)
checkpoint.run(binding.id)
return binding
StoreImport = StoreImporter # deprecated
[docs]@magento
class StoreviewImporter(MagentoImporter):
""" Import one Magento Storeview """
_model_name = ['magento.storeview',
]
def _create(self, data):
binding = super(StoreviewImporter, self)._create(data)
checkpoint = self.unit_for(StoreAddCheckpoint)
checkpoint.run(binding.id)
return binding
StoreviewImport = StoreviewImporter # deprecated
[docs]@magento
class StoreAddCheckpoint(ConnectorUnit):
""" Add a connector.checkpoint on the magento.storeview
or magento.store record
"""
_model_name = ['magento.storeview',
'magento.store',
]
[docs] def run(self, binding_id):
add_checkpoint(self.session,
self.model._name,
binding_id,
self.backend_record.id)
# backward compatibility
StoreViewAddCheckpoint = magento(StoreAddCheckpoint)