diff --git a/__unported__/connector_ecommerce/payment_method.py b/__unported__/connector_ecommerce/payment_method.py
deleted file mode 100644
index 928fc148..00000000
--- a/__unported__/connector_ecommerce/payment_method.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# Copyright 2011-2013 Akretion
-# @author Sébastien BEAU
-#
-# 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 .
-#
-##############################################################################
-
-from openerp.osv import orm, fields
-
-
-class payment_method(orm.Model):
- _inherit = "payment.method"
-
- def _get_import_rules(self, cr, uid, context=None):
- return [('always', 'Always'),
- ('never', 'Never'),
- ('paid', 'Paid'),
- ('authorized', 'Authorized'),
- ]
-
- def __get_import_rules(self, cr, uid, context=None):
- return self._get_import_rules(cr, uid, context=context)
-
- _columns = {
- # the logic around the 2 following fields has to be implemented
- # in the connectors (magentoerpconnect, prestashoperpconnect,...)
- 'days_before_cancel': fields.integer(
- 'Days before cancel',
- help="After 'n' days, if the 'Import Rule' is not fulfilled, the "
- "import of the sale order will be canceled."),
- 'import_rule': fields.selection(__get_import_rules,
- string="Import Rule",
- required=True)
- }
-
- _defaults = {
- 'import_rule': 'always',
- 'days_before_cancel': 30,
- }
-
- def get_or_create_payment_method(self, cr, uid, payment_method,
- context=None):
- """
- try to get id of 'payment_method' or create if not exists
- :param str payment_method: payment method like PayPal, etc.
- :rtype: int
- :return: id of required payment method
- """
- pay_method_obj = self.pool.get('payment.method')
- domain = [('name', '=ilike', payment_method)]
- method_ids = pay_method_obj.search(cr, uid, domain, context=context)
- if method_ids:
- method_id = method_ids[0]
- else:
- method_id = pay_method_obj.create(cr, uid,
- {'name': payment_method},
- context=context)
- return method_id
diff --git a/__unported__/connector_ecommerce/product.py b/__unported__/connector_ecommerce/product.py
deleted file mode 100644
index dcf88ef2..00000000
--- a/__unported__/connector_ecommerce/product.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# Author: Sébastien BEAU
-# Copyright 2011-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 .
-#
-##############################################################################
-
-from openerp.osv import orm, fields
-from openerp.addons.connector.session import ConnectorSession
-from .event import on_product_price_changed
-
-
-class product_template(orm.Model):
- _inherit = 'product.template'
-
- # TODO implement set function and also support multi tax
- def _get_tax_group_id(self, cr, uid, ids, field_name, args, context=None):
- result = {}
- for product in self.browse(cr, uid, ids, context=context):
- taxes = product.taxes_id
- result[product.id] = taxes[0].group_id.id if taxes else False
- return result
-
- _columns = {
- 'tax_group_id': fields.function(
- _get_tax_group_id,
- string='Tax Group',
- type='many2one',
- relation='account.tax.group',
- store=False,
- help='Tax group are used with some external '
- 'system like Prestashop'),
- }
-
- def _price_changed(self, cr, uid, ids, vals, context=None):
- """ Fire the ``on_product_price_changed`` on all the variants of
- the template if the price if the product could have changed.
-
- If one of the field used in a sale pricelist item has been
- modified, we consider that the price could have changed.
-
- There is no guarantee that's the price actually changed,
- because it depends on the pricelists.
- """
- if context is None:
- context = {}
- type_obj = self.pool['product.price.type']
- price_fields = type_obj.sale_price_fields(cr, uid, context=context)
- # restrict the fields to the template ones only, so if
- # the write has been done on product.product, we won't
- # update all the variant if a price field of the
- # variant has been changed
- tmpl_fields = [field for field in vals if field in self._columns]
- if any(field in price_fields for field in tmpl_fields):
- product_obj = self.pool['product.product']
- session = ConnectorSession(cr, uid, context=context)
- product_ids = product_obj.search(cr, uid,
- [('product_tmpl_id', 'in', ids)],
- context=context)
- # when the write is done on the product.product, avoid
- # to fire the event 2 times
- if context.get('from_product_ids'):
- product_ids = list(set(product_ids) -
- set(context['from_product_ids']))
- for prod_id in product_ids:
- on_product_price_changed.fire(session,
- product_obj._name,
- prod_id)
-
- def write(self, cr, uid, ids, vals, context=None):
- if isinstance(ids, (int, long)):
- ids = [ids]
- result = super(product_template, self).write(cr, uid, ids,
- vals, context=context)
- self._price_changed(cr, uid, ids, vals, context=context)
- return result
-
-
-class product_product(orm.Model):
- _inherit = 'product.product'
-
- def _get_checkpoint(self, cr, uid, ids, name, arg, context=None):
- result = {}
- checkpoint_obj = self.pool.get('connector.checkpoint')
- model_obj = self.pool.get('ir.model')
- model_id = model_obj.search(cr, uid,
- [('model', '=', 'product.product')],
- context=context)[0]
- for product_id in ids:
- point_ids = checkpoint_obj.search(cr, uid,
- [('model_id', '=', model_id),
- ('record_id', '=', product_id),
- ('state', '=', 'need_review')],
- context=context)
- result[product_id] = bool(point_ids)
- return result
-
- _columns = {
- 'has_checkpoint': fields.function(_get_checkpoint,
- type='boolean',
- readonly=True,
- string='Has Checkpoint'),
- }
-
- def _price_changed(self, cr, uid, ids, vals, context=None):
- """ Fire the ``on_product_price_changed`` if the price
- if the product could have changed.
-
- If one of the field used in a sale pricelist item has been
- modified, we consider that the price could have changed.
-
- There is no guarantee that's the price actually changed,
- because it depends on the pricelists.
- """
- type_obj = self.pool['product.price.type']
- price_fields = type_obj.sale_price_fields(cr, uid, context=context)
- if any(field in price_fields for field in vals):
- session = ConnectorSession(cr, uid, context=context)
- for prod_id in ids:
- on_product_price_changed.fire(session, self._name, prod_id)
-
- def write(self, cr, uid, ids, vals, context=None):
- if context is None:
- context = {}
- if isinstance(ids, (int, long)):
- ids = [ids]
- context = context.copy()
- context['from_product_ids'] = ids
- result = super(product_product, self).write(
- cr, uid, ids, vals, context=context)
- self._price_changed(cr, uid, ids, vals, context=context)
- return result
-
- def create(self, cr, uid, vals, context=None):
- product_ids = super(product_product, self).create(
- cr, uid, vals, context=context)
- self._price_changed(cr, uid, [product_ids], vals, context=context)
- return product_ids
-
-
-class product_price_type(orm.Model):
- _inherit = 'product.price.type'
-
- _columns = {
- 'pricelist_item_ids': fields.one2many(
- 'product.pricelist.item', 'base',
- string='Pricelist Items',
- readonly=True)
- }
-
- def sale_price_fields(self, cr, uid, context=None):
- """ Returns a list of fields used by sale pricelists.
- Used to know if the sale price could have changed
- when one of these fields has changed.
- """
- item_obj = self.pool['product.pricelist.item']
- item_ids = item_obj.search(
- cr, uid,
- [('price_version_id.pricelist_id.type', '=', 'sale')],
- context=context)
- type_ids = self.search(cr, uid,
- [('pricelist_item_ids', 'in', item_ids)],
- context=context)
- types = self.read(cr, uid, type_ids, ['field'], context=context)
- return [t['field'] for t in types]
diff --git a/__unported__/connector_ecommerce/sale.py b/__unported__/connector_ecommerce/sale.py
deleted file mode 100644
index f6526bcc..00000000
--- a/__unported__/connector_ecommerce/sale.py
+++ /dev/null
@@ -1,525 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# Author: Guewen Baconnier
-# Copyright 2011-2013 Camptocamp SA
-# Author: Sébastien Beau
-# Copyright 2010-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 .
-#
-##############################################################################
-
-import logging
-
-from openerp.osv import orm, fields, osv
-from openerp.tools.translate import _
-from openerp import netsvc
-from openerp.addons.connector.connector import ConnectorUnit
-
-_logger = logging.getLogger(__name__)
-
-
-class sale_shop(orm.Model):
- _inherit = 'sale.shop'
-
- def _get_payment_default(self, cr, uid, context=None):
- """ Return a arbitrary account.payment.term record for the sale.shop
-
- ``sale.shop`` records are created dynamically from the backends
- and the field ``payment_default_id`` needs a default value.
- """
- data_obj = self.pool.get('ir.model.data')
- __, payment_id = data_obj.get_object_reference(
- cr, uid, 'account', 'account_payment_term_immediate')
- return payment_id
-
- _defaults = {
- # see method docstring for explanation
- 'payment_default_id': _get_payment_default,
- }
-
-
-class sale_order(orm.Model):
- """ Add a cancellation mecanism in the sales orders
-
- When a sale order is canceled in a backend, the connectors can flag
- the 'canceled_in_backend' flag. It will:
-
- * try to automatically cancel the sales order
- * block the confirmation of the sales orders using a 'sales exception'
-
- When a sales order is canceled or the user used the button to force
- to 'keep it open', the flag 'cancellation_resolved' is set to True.
-
- The second axe which can be used by the connectors is the 'parent'
- sale order. When a sales order has a parent sales order (logic to
- link with the parent to be defined by each connector), it will be
- blocked until the cancellation of the sale order is resolved.
-
- This is used by, for instance, the magento connector, when one
- modifies a sales order, Magento cancels it and create a new one with
- the first one as parent.
- """
- _inherit = 'sale.order'
-
- def _get_parent_id(self, cr, uid, ids, name, arg, context=None):
- return self.get_parent_id(cr, uid, ids, context=context)
-
- def get_parent_id(self, cr, uid, ids, context=None):
- """ Need to be inherited in the connectors to implement the
- parent logic.
-
- See an implementation example in ``magentoerpconnect``.
- """
- return dict.fromkeys(ids, False)
-
- def _get_need_cancel(self, cr, uid, ids, name, arg, context=None):
- result = {}
- for order in self.browse(cr, uid, ids, context=context):
- result[order.id] = self._need_cancel(cr, uid, order,
- context=context)
- return result
-
- def _get_parent_need_cancel(self, cr, uid, ids, name, arg, context=None):
- result = {}
- for order in self.browse(cr, uid, ids, context=context):
- result[order.id] = self._parent_need_cancel(cr, uid, order,
- context=context)
- return result
-
- _columns = {
- 'canceled_in_backend': fields.boolean('Canceled in backend',
- readonly=True),
- # set to True when the cancellation from the backend is
- # resolved, either because the SO has been canceled or
- # because the user manually chosed to keep it open
- 'cancellation_resolved': fields.boolean('Cancellation from the '
- 'backend resolved'),
- 'parent_id': fields.function(_get_parent_id,
- string='Parent Order',
- type='many2one',
- help='A parent sales order is a sales '
- 'order replaced by this one.',
- relation='sale.order'),
- 'need_cancel': fields.function(_get_need_cancel,
- string='Need to be canceled',
- type='boolean',
- help='Has been canceled on the backend'
- ', need to be canceled.'),
- 'parent_need_cancel': fields.function(
- _get_parent_need_cancel,
- string='A parent sales orders needs cancel',
- type='boolean',
- help='A parent sales orders has been canceled on the backend'
- ' and needs to be canceled.'),
- }
-
- def _need_cancel(self, cr, uid, order, context=None):
- """ Return True if the sales order need to be canceled
- (has been canceled on the Backend) """
- return order.canceled_in_backend and not order.cancellation_resolved
-
- def _parent_need_cancel(self, cr, uid, order, context=None):
- """ Return True if at least one parent sales order need to
- be canceled (has been canceled on the backend).
- Follows all the parent sales orders.
- """
- def need_cancel(order):
- if self._need_cancel(cr, uid, order, context=context):
- return True
- if order.parent_id:
- return need_cancel(order.parent_id)
- else:
- return False
- if not order.parent_id:
- return False
- return need_cancel(order.parent_id)
-
- def _try_auto_cancel(self, cr, uid, ids, context=None):
- """ Try to automatically cancel a sales order canceled
- in a backend.
-
- If it can't cancel it, does nothing.
- """
- wkf_states = ('draft', 'sent')
- action_states = ('manual', 'progress')
- wf_service = netsvc.LocalService("workflow")
- resolution_msg = _("Resolution:
"
- "- Cancel the linked invoices, delivery "
- "orders, automatic payments.
"
- "- Cancel the sales order manually.
"
- "
")
- for order_id in ids:
- state = self.read(cr, uid, order_id,
- ['state'], context=context)['state']
- if state == 'cancel':
- continue
- elif state == 'done':
- message = _("The sales order cannot be automatically "
- "canceled because it is already done.")
- elif state in wkf_states + action_states:
- try:
- # respect the same cancellation methods than
- # the sales order view: quotations use the workflow
- # action, sales orders use the action_cancel method.
- if state in wkf_states:
- wf_service.trg_validate(uid, 'sale.order',
- order_id, 'cancel', cr)
- elif state in action_states:
- self.action_cancel(cr, uid, order_id, context=context)
- else:
- raise ValueError('%s should not fall here.' % state)
- except osv.except_osv:
- # the 'cancellation_resolved' flag will stay to False
- message = _("The sales order could not be automatically "
- "canceled.") + resolution_msg
- else:
- message = _("The sales order has been automatically "
- "canceled.")
- else:
- # shipping_except, invoice_except, ...
- # can not be canceled from the view, so assume that it
- # should not be canceled here neiter, exception to
- # resolve
- message = _("The sales order could not be automatically "
- "canceled for this status.") + resolution_msg
- self.message_post(cr, uid, [order_id], body=message,
- context=context)
-
- def _log_canceled_in_backend(self, cr, uid, ids, context=None):
- message = _("The sales order has been canceled on the backend.")
- self.message_post(cr, uid, ids, body=message, context=context)
- for order in self.browse(cr, uid, ids, context=context):
- message = _("Warning: the origin sales order %s has been canceled "
- "on the backend.") % order.name
- if order.picking_ids:
- picking_obj = self.pool.get('stock.picking')
- picking_obj.message_post(cr, uid, order.picking_ids,
- body=message, context=context)
- if order.invoice_ids:
- picking_obj = self.pool.get('account.invoice')
- picking_obj.message_post(cr, uid, order.invoice_ids,
- body=message, context=context)
-
- def create(self, cr, uid, values, context=None):
- order_id = super(sale_order, self).create(cr, uid, values,
- context=context)
- if values.get('canceled_in_backend'):
- self._log_canceled_in_backend(cr, uid, [order_id], context=context)
- self._try_auto_cancel(cr, uid, [order_id], context=context)
- return order_id
-
- def write(self, cr, uid, ids, values, context=None):
- result = super(sale_order, self).write(cr, uid, ids, values,
- context=context)
- if values.get('canceled_in_backend'):
- self._log_canceled_in_backend(cr, uid, ids, context=context)
- self._try_auto_cancel(cr, uid, ids, context=context)
- return result
-
- def action_cancel(self, cr, uid, ids, context=None):
- if not hasattr(ids, '__iter__'):
- ids = [ids]
- super(sale_order, self).action_cancel(cr, uid, ids, context=context)
- sales = self.read(cr, uid, ids,
- ['canceled_in_backend',
- 'cancellation_resolved'],
- context=context)
- for sale in sales:
- # the sale order is canceled => considered as resolved
- if (sale['canceled_in_backend'] and
- not sale['cancellation_resolved']):
- self.write(cr, uid, sale['id'],
- {'cancellation_resolved': True},
- context=context)
- return True
-
- def ignore_cancellation(self, cr, uid, ids, reason, context=None):
- """ Manually set the cancellation from the backend as resolved.
-
- The user can choose to keep the sale order active for some reason,
- so it just push a button to keep it alive.
- """
- message = (_("Despite the cancellation of the sales order on the "
- "backend, it should stay open.
Reason: %s") %
- reason)
- self.message_post(cr, uid, ids, body=message, context=context)
- self.write(cr, uid, ids,
- {'cancellation_resolved': True},
- context=context)
- return True
-
- def action_view_parent(self, cr, uid, ids, context=None):
- """ Return an action to display the parent sale order """
- if isinstance(ids, (list, tuple)):
- assert len(ids) == 1
- ids = ids[0]
-
- mod_obj = self.pool.get('ir.model.data')
- act_obj = self.pool.get('ir.actions.act_window')
-
- parent = self.browse(cr, uid, ids, context=context).parent_id
- if not parent:
- return
-
- view_xmlid = ('sale', 'view_order_form')
- if parent.state in ('draft', 'sent', 'cancel'):
- action_xmlid = ('sale', 'action_quotations')
- else:
- action_xmlid = ('sale', 'action_orders')
-
- ref = mod_obj.get_object_reference(cr, uid, *action_xmlid)
- action_id = False
- if ref:
- __, action_id = ref
- action = act_obj.read(cr, uid, [action_id], context=context)[0]
-
- ref = mod_obj.get_object_reference(cr, uid, *view_xmlid)
- action['views'] = [(ref[1] if ref else False, 'form')]
- action['res_id'] = parent.id
- return action
-
- # TODO: remove in odoo 8.0
- def _convert_special_fields(self, cr, uid, vals, order_lines,
- context=None):
- """ Deprecated! Replaced by LineBuilder classes
-
- Convert the special 'fake' field into an order line.
-
- Special fields are:
- - shipping amount and shipping_tax_rate
- - cash_on_delivery and cash_on_delivery_taxe_rate
- - gift_certificates
-
- :param vals: values of the sale order to create
- :type vals: dict
- :param order_lines: lines of the orders to import
- :return: the value for the sale order with the special field converted
- :rtype: dict
- """
- _logger.warning('sale_order._convert_special_fields() has been '
- 'deprecated. Use a specialized '
- 'SpecialOrderLineBuilder class instead.')
- shipping_fields = ['shipping_amount_tax_excluded',
- 'shipping_amount_tax_included',
- 'shipping_tax_amount']
-
- def check_key(keys):
- return len(set(shipping_fields) & set(keys)) >= 2
-
- vals.setdefault('order_line', [])
- for line in order_lines:
- for field in shipping_fields:
- if field in line[2]:
- vals[field] = vals.get(field, 0.0) + line[2][field]
- del line[2][field]
-
- if 'shipping_tax_rate' not in vals and check_key(vals.keys()):
- if 'shipping_amount_tax_excluded' not in vals:
- amount_incl = vals['shipping_amount_tax_included']
- tax_amount = vals['shipping_tax_amount']
- tax_excl = (amount_incl - tax_amount)
- vals['shipping_amount_tax_excluded'] = tax_excl
-
- elif 'shipping_tax_amount' not in vals:
- amount_incl = vals['shipping_amount_tax_included']
- amount_excl = vals['shipping_amount_tax_excluded']
- vals['shipping_tax_amount'] = (amount_incl - amount_excl)
- if vals['shipping_amount_tax_excluded']:
- tax_amount = vals['shipping_tax_amount']
- tax_amount_excl = vals['shipping_amount_tax_excluded']
- vals['shipping_tax_rate'] = (tax_amount / tax_amount_excl)
- else:
- vals['shipping_tax_rate'] = 0.
- del vals['shipping_tax_amount']
- for option in self._get_special_fields(cr, uid, context=context):
- vals = self._add_order_extra_line(cr, uid, vals,
- option, context=context)
- return vals
-
- # TODO: remove in odoo 8.0
- def _get_special_fields(self, cr, uid, context=None):
- """ Deprecated! Replaced by LineBuilder classes """
- return [
- {'price_unit_tax_excluded': 'shipping_amount_tax_excluded',
- 'price_unit_tax_included': 'shipping_amount_tax_included',
- 'tax_rate_field': 'shipping_tax_rate',
- 'product_ref': ('connector_ecommerce',
- 'product_product_shipping'),
- },
- {'tax_rate_field': 'cash_on_delivery_taxe_rate',
- 'price_unit_tax_excluded': 'cash_on_delivery_amount_tax_excluded',
- 'price_unit_tax_included': 'cash_on_delivery_amount_tax_included',
- 'product_ref': ('connector_ecommerce',
- 'product_product_cash_on_delivery'),
- },
- # gift certificate doesn't have any tax
- {'price_unit_tax_excluded': 'gift_certificates_amount',
- 'price_unit_tax_included': 'gift_certificates_amount',
- 'product_ref': ('connector_ecommerce', 'product_product_gift'),
- 'code_field': 'gift_certificates_code',
- 'sign': -1,
- },
- ]
-
- # TODO: remove in odoo 8.0
- def _get_order_extra_line_vals(self, cr, uid, vals, option, product,
- price_unit, context=None):
- """ Deprecated! Replaced by LineBuilder classes """
- return {
- 'product_id': product.id,
- 'name': product.name,
- 'product_uom': product.uom_id.id,
- 'product_uom_qty': 1,
- 'price_unit': price_unit
- }
-
- # TODO: remove in odoo 8.0
- def _add_order_extra_line(self, cr, uid, vals, option, context=None):
- """ Deprecated! Replaced by LineBuilder classes
-
- Add or substract amount on order as a separate line item
- with single quantity for each type of amounts like: shipping,
- cash on delivery, discount, gift certificates...
-
- :param dict vals: values of the sale order to create
- :param option: dictionary of options for the special field to process
- """
- if context is None:
- context = {}
- sign = option.get('sign', 1)
- if (context.get('is_tax_included') and
- vals.get(option['price_unit_tax_included'])):
- price_unit = vals.pop(option['price_unit_tax_included']) * sign
- elif vals.get(option['price_unit_tax_excluded']):
- price_unit = vals.pop(option['price_unit_tax_excluded']) * sign
- else:
- return self._clean_special_fields(option, vals)
- model_data_obj = self.pool.get('ir.model.data')
- product_obj = self.pool.get('product.product')
- __, product_id = model_data_obj.get_object_reference(
- cr, uid, *option['product_ref'])
- product = product_obj.browse(cr, uid, product_id, context=context)
-
- extra_line = self._get_order_extra_line_vals(
- cr, uid, vals, option, product, price_unit, context=context)
-
- ext_code_field = option.get('code_field')
- if ext_code_field and vals.get(ext_code_field):
- extra_line['name'] = "%s [%s]" % (extra_line['name'],
- vals[ext_code_field])
- vals['order_line'].append((0, 0, extra_line))
- return self._clean_special_fields(option, vals)
-
- # TODO: remove in odoo 8.0
- def _clean_special_fields(self, option, vals):
- """ Deprecated! Replaced by LineBuilder classes """
- for key in ['price_unit_tax_excluded',
- 'price_unit_tax_included',
- 'tax_rate_field']:
- if option.get(key) and option[key] in vals:
- del vals[option[key]]
- return vals # if there is no price, we have nothing to import
-
-
-class SpecialOrderLineBuilder(ConnectorUnit):
- """ Base class to build a sale order line for a sale order
-
- Used when extra order lines have to be added in a sale order
- but we only know some parameters (product, price, ...), for instance,
- a line for the shipping costs or the gift coupons.
-
- It can be subclassed to customize the way the lines are created.
-
- Usage::
-
- builder = self.get_connector_for_unit(ShippingLineBuilder,
- model='sale.order.line')
- builder.price_unit = 100
- builder.get_line()
-
- """
- _model_name = None
-
- def __init__(self, environment):
- super(SpecialOrderLineBuilder, self).__init__(environment)
- self.product = None # id or browse_record
- # when no product_id, fallback to a product_ref
- self.product_ref = None # tuple (module, xmlid)
- self.price_unit = None
- self.quantity = 1
- self.sign = 1
- self.sequence = 980
-
- def get_line(self):
- assert self.product_ref or self.product
- assert self.price_unit is not None
- session = self.session
-
- product = product_id = self.product
- if product_id is None:
- model_data_obj = session.pool.get('ir.model.data')
- __, product_id = model_data_obj.get_object_reference(
- session.cr, session.uid, *self.product_ref)
-
- if not isinstance(product_id, orm.browse_record):
- product = session.browse('product.product', product_id)
- return {'product_id': product.id,
- 'name': product.name,
- 'product_uom': product.uom_id.id,
- 'product_uom_qty': self.quantity,
- 'price_unit': self.price_unit * self.sign,
- 'sequence': self.sequence}
-
-
-class ShippingLineBuilder(SpecialOrderLineBuilder):
- """ Return values for a Shipping line """
- _model_name = None
-
- def __init__(self, environment):
- super(ShippingLineBuilder, self).__init__(environment)
- self.product_ref = ('connector_ecommerce', 'product_product_shipping')
- self.sequence = 999
-
-
-class CashOnDeliveryLineBuilder(SpecialOrderLineBuilder):
- """ Return values for a Cash on Delivery line """
- _model_name = None
- _model_name = None
-
- def __init__(self, environment):
- super(CashOnDeliveryLineBuilder, self).__init__(environment)
- self.product_ref = ('connector_ecommerce',
- 'product_product_cash_on_delivery')
- self.sequence = 995
-
-
-class GiftOrderLineBuilder(SpecialOrderLineBuilder):
- """ Return values for a Gift line """
- _model_name = None
-
- def __init__(self, environment):
- super(GiftOrderLineBuilder, self).__init__(environment)
- self.product_ref = ('connector_ecommerce',
- 'product_product_gift')
- self.sign = -1
- self.gift_code = None
- self.sequence = 990
-
- def get_line(self):
- line = super(GiftOrderLineBuilder, self).get_line()
- if self.gift_code:
- line['name'] = "%s [%s]" % (line['name'], self.gift_code)
- return line
diff --git a/__unported__/connector_ecommerce/stock.py b/__unported__/connector_ecommerce/stock.py
deleted file mode 100644
index 121633f0..00000000
--- a/__unported__/connector_ecommerce/stock.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# Author: Joel Grand-Guillaume
-# Copyright 2013 Camptocamp SA
-#
-# 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 .
-#
-##############################################################################
-
-from openerp.osv import orm, fields
-
-from openerp.addons.connector.session import ConnectorSession
-from .event import on_picking_out_done, on_tracking_number_added
-
-
-class stock_picking(orm.Model):
- _inherit = 'stock.picking'
-
- _columns = {
- 'related_backorder_ids': fields.one2many(
- 'stock.picking', 'backorder_id',
- string="Related backorders"),
- }
-
- def action_done(self, cr, uid, ids, context=None):
- res = super(stock_picking, self).action_done(cr, uid,
- ids, context=context)
- session = ConnectorSession(cr, uid, context=context)
- # Look if it exists a backorder, in that case call for partial
- picking_records = self.read(cr, uid, ids,
- ['id', 'related_backorder_ids', 'type'],
- context=context)
- for picking_vals in picking_records:
- if picking_vals['type'] != 'out':
- continue
- if picking_vals['related_backorder_ids']:
- picking_method = 'partial'
- else:
- picking_method = 'complete'
- on_picking_out_done.fire(session, self._name,
- picking_vals['id'], picking_method)
- return res
-
- def copy(self, cr, uid, id, default=None, context=None):
- if default is None:
- default = {}
- else:
- default = default.copy()
- default['related_backorder_ids'] = False
- return super(stock_picking, self).copy(cr, uid,
- id, default, context=context)
-
-
-class stock_picking_out(orm.Model):
- _inherit = 'stock.picking.out'
-
- def write(self, cr, uid, ids, vals, context=None):
- if not hasattr(ids, '__iter__'):
- ids = [ids]
- res = super(stock_picking_out, self).write(cr, uid, ids,
- vals, context=context)
- if vals.get('carrier_tracking_ref'):
- session = ConnectorSession(cr, uid, context=context)
- for record_id in ids:
- on_tracking_number_added.fire(session, self._name, record_id)
- return res
diff --git a/__unported__/connector_ecommerce/stock_view.xml b/__unported__/connector_ecommerce/stock_view.xml
deleted file mode 100644
index cc102b30..00000000
--- a/__unported__/connector_ecommerce/stock_view.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- stock.picking.out.connector.form
- stock.picking.out
-
-
-
-
-
-
-
-
-
-
-
diff --git a/__unported__/connector_ecommerce/tests/test_onchange.py b/__unported__/connector_ecommerce/tests/test_onchange.py
deleted file mode 100644
index f857f050..00000000
--- a/__unported__/connector_ecommerce/tests/test_onchange.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# -*- coding: utf-8 -*-
-###############################################################################
-#
-# connector-ecommerce for OpenERP
-# Copyright (C) 2013-TODAY Akretion .
-# @author Sébastien BEAU
-#
-# 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 .
-#
-###############################################################################
-
-import mock
-
-from openerp.addons.connector_ecommerce.unit.sale_order_onchange import (
- SaleOrderOnChange)
-from openerp.addons.connector.session import ConnectorSession
-from openerp.addons.connector.connector import Environment
-import openerp.tests.common as common
-
-DB = common.DB
-ADMIN_USER_ID = common.ADMIN_USER_ID
-
-
-class test_onchange(common.TransactionCase):
- """ Test if the onchanges are applied correctly on a sale order"""
-
- def setUp(self):
- super(test_onchange, self).setUp()
- self.session = ConnectorSession(self.cr, self.uid)
-
- def test_play_onchange(self):
- """ Play the onchange ConnectorUnit on a sale order """
- product_model = self.registry('product.product')
- partner_model = self.registry('res.partner')
- shop_model = self.registry('sale.shop')
- tax_model = self.registry('account.tax')
- cr, uid = self.cr, self.uid
-
- backend_record = mock.Mock()
- env = Environment(backend_record, self.session, 'sale.order')
-
- partner_id = partner_model.create(cr, uid,
- {'name': 'seb',
- 'zip': '69100',
- 'city': 'Villeurbanne'})
- partner_invoice_id = partner_model.create(cr, uid,
- {'name': 'Guewen',
- 'zip': '1015',
- 'city': 'Lausanne',
- 'type': 'invoice',
- 'parent_id': partner_id})
- tax_id = tax_model.create(cr, uid, {'name': 'My Tax'})
- product_id = product_model.create(cr, uid,
- {'default_code': 'MyCode',
- 'name': 'My Product',
- 'weight': 15,
- 'taxes_id': [(6, 0, [tax_id])]})
- shop_id = shop_model.create(cr, uid, {'name': 'My shop'})
-
- order_input = {
- 'shop_id': shop_id,
- 'name': 'mag_10000001',
- 'partner_id': partner_id,
- 'order_line': [
- (0, 0, {'product_id': product_id,
- 'price_unit': 20,
- 'name': 'My Real Name',
- 'product_uom_qty': 1,
- }
- ),
- ]
- }
-
- onchange = SaleOrderOnChange(env)
- order = onchange.play(order_input,
- order_input['order_line'])
-
- self.assertEqual(order['partner_invoice_id'], partner_invoice_id)
- line = order['order_line'][0][2]
- self.assertEqual(line['name'], 'My Real Name')
- self.assertEqual(line['th_weight'], 15)
- self.assertEqual(line['tax_id'][0][2][0], tax_id)
diff --git a/__unported__/connector_ecommerce/unit/__init__.py b/__unported__/connector_ecommerce/unit/__init__.py
deleted file mode 100644
index b9150933..00000000
--- a/__unported__/connector_ecommerce/unit/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# -*- coding: utf-8 -*-
-import sale_order_onchange
diff --git a/__unported__/connector_ecommerce/unit/sale_order_onchange.py b/__unported__/connector_ecommerce/unit/sale_order_onchange.py
deleted file mode 100644
index 06702067..00000000
--- a/__unported__/connector_ecommerce/unit/sale_order_onchange.py
+++ /dev/null
@@ -1,265 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# connector-ecommerce for OpenERP
-# Copyright (C) 2013-TODAY Akretion .
-# @author Sébastien BEAU
-#
-# 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 .
-#
-##############################################################################
-
-from openerp.addons.connector.connector import ConnectorUnit
-
-
-class OnChangeManager(ConnectorUnit):
- def merge_values(self, record, on_change_result):
- vals = on_change_result.get('value', {})
- for key in vals:
- if key not in record:
- record[key] = vals[key]
-
-
-class SaleOrderOnChange(OnChangeManager):
- _model_name = None
-
- def _get_partner_id_onchange_param(self, order):
- """ Prepare the arguments for calling the partner_id change
- on sale order. You can overwrite this method in your own
- module if they modify the onchange signature
-
- :param order: a dictionary of the value of your sale order
- :type: dict
-
- :return: a tuple of args and kwargs for the onchange
- :rtype: tuple
- """
- args = [
- None, # sale order ids not needed
- order['partner_id'],
- ]
- kwargs = {'context': self.session.context}
- return args, kwargs
-
- def _get_shop_id_onchange_param(self, order):
- args = [None,
- order['shop_id']]
- kwargs = {'context': self.session.context}
- return args, kwargs
-
- def _get_payment_method_id_onchange_param(self, order):
- args = [None,
- order['payment_method_id']]
- kwargs = {'context': self.session.context}
- return args, kwargs
-
- def _get_workflow_process_id_onchange_param(self, order):
- args = [None,
- order['workflow_process_id']]
- kwargs = {'context': self.session.context}
- return args, kwargs
-
- def _get_partner_address_id_onchange_param(self, order):
- args = [
- None,
- order['partner_invoice_id'],
- order['partner_shipping_id'],
- order['partner_id'],
- order['shop_id'],
- ]
- kwargs = {'context': self.session.context}
- return args, kwargs
-
- def _play_order_onchange(self, order):
- """ Play the onchange of the sale order
-
- :param order: a dictionary of the value of your sale order
- :type: dict
-
- :return: the value of the sale order updated with the onchange result
- :rtype: dict
- """
- sale_model = self.session.pool.get('sale.order')
-
- # Play partner_id onchange
- args, kwargs = self._get_shop_id_onchange_param(order)
- res = sale_model.onchange_shop_id(self.session.cr,
- self.session.uid,
- *args,
- **kwargs)
- self.merge_values(order, res)
-
- args, kwargs = self._get_partner_id_onchange_param(order)
- res = sale_model.onchange_partner_id(self.session.cr,
- self.session.uid,
- *args,
- **kwargs)
- self.merge_values(order, res)
-
- if order.get('payment_method_id'):
- # apply payment method
- args, kwargs = self._get_payment_method_id_onchange_param(order)
- res = sale_model.onchange_payment_method_id(self.session.cr,
- self.session.uid,
- *args,
- **kwargs)
- # If the onchange return a False fiscal position
- # We do not merge it, because the onchange on the address
- # will not set correctly the fiscal position as the field
- # already exists in the order dict
- if res.get('value') and 'fiscal_position' in res['value']:
- if not res['value']['fiscal_position']:
- res['value'].pop('fiscal_position')
-
- self.merge_values(order, res)
-
- if order.get('workflow_process_id'):
- # apply default values from the workflow
- args, kwargs = self._get_workflow_process_id_onchange_param(order)
- res = sale_model.onchange_workflow_process_id(self.session.cr,
- self.session.uid,
- *args,
- **kwargs)
- self.merge_values(order, res)
-
- # Play onchange on address
- if hasattr(sale_model, 'onchange_address_id'):
- args, kwargs = self._get_partner_address_id_onchange_param(order)
- res = sale_model.onchange_address_id(self.session.cr,
- self.session.uid,
- *args,
- **kwargs)
- if res.get('value') and 'fiscal_position' in res['value']:
- order['fiscal_position'] = res['value']['fiscal_position']
- self.merge_values(order, res)
- return order
-
- def _get_product_id_onchange_param(self, line, previous_lines, order):
- """ Prepare the arguments for calling the product_id change
- on sale order line. You can overwrite this method in your own
- module if they modify the onchange signature
-
- :param line: the sale order line to process
- :type: dict
- :param previous_lines: list of dict of the previous lines processed
- :type: list
- :param order: data of the sale order
- :type: dict
-
- :return: a tuple of args and kwargs for the onchange
- :rtype: tuple
- """
- args = [
- None, # sale order line ids not needed
- order.get('pricelist_id'),
- line.get('product_id')
- ]
-
- # used in sale_markup: this is to ensure the unit price
- # sent by the e-commerce connector is used for markup calculation
- onchange_context = self.session.context.copy()
- if line.get('unit_price', False):
- onchange_context.update({'unit_price': line['unit_price'],
- 'force_unit_price': True})
-
- uos_qty = float(line.get('product_uos_qty', 0))
- if not uos_qty:
- uos_qty = float(line.get('product_uom_qty', 0))
-
- kwargs = {
- 'qty': float(line.get('product_uom_qty', 0)),
- 'uom': line.get('product_uom'),
- 'qty_uos': uos_qty,
- 'uos': line.get('product_uos'),
- 'name': line.get('name'),
- 'partner_id': order.get('partner_id'),
- 'lang': False,
- 'update_tax': True,
- 'date_order': order.get('date_order'),
- 'packaging': line.get('product_packaging'),
- 'fiscal_position': order.get('fiscal_position'),
- 'flag': False,
- 'context': onchange_context,
- }
- return args, kwargs
-
- def _play_line_onchange(self, line, previous_lines, order):
- """ Play the onchange of the sale order line
-
- :param line: the sale order line to process
- :type: dict
- :param previous_lines: list of dict of the previous line processed
- :type: list
- :param order: data of the sale order
- :type: dict
-
- :return: the value of the sale order updated with the onchange result
- :rtype: dict
- """
- sale_line_model = self.session.pool.get('sale.order.line')
-
- # Play product_id onchange
- args, kwargs = self._get_product_id_onchange_param(line,
- previous_lines,
- order)
- res = sale_line_model.product_id_change(self.session.cr,
- self.session.uid,
- *args,
- **kwargs)
- # TODO refactor this with merge_values
- vals = res.get('value', {})
- for key in vals:
- if key not in line:
- if sale_line_model._columns[key]._type == 'many2many':
- line[key] = [(6, 0, vals[key])]
- else:
- line[key] = vals[key]
- return line
-
- def play(self, order, order_lines):
- """ Play the onchange of the sale order and it's lines
-
- :param order: data of the sale order
- :type: dict
-
- :return: the value of the sale order updated with the onchange result
- :rtype: dict
- """
- # play onchange on sale order
- with self.session.change_context(dict(shop_id=order.get('shop_id'))):
- order = self._play_order_onchange(order)
- # play onchange on sale order line
- processed_order_lines = []
- line_lists = [order_lines]
- if 'order_line' in order and order['order_line'] is not order_lines:
- # we have both backend-dependent and oerp-native order
- # lines.
- # oerp-native lines can have been added to map
- # shipping fees with an OpenERP Product
- line_lists.append(order['order_line'])
- for line_list in line_lists:
- for idx, command_line in enumerate(line_list):
- # line_list format:[(0, 0, {...}), (0, 0, {...})]
- if command_line[0] in (0, 1): # create or update values
- # keeps command number and ID (or 0)
- old_line_data = command_line[2]
- new_line_data = self._play_line_onchange(
- old_line_data, processed_order_lines, order)
- new_line = (command_line[0],
- command_line[1],
- new_line_data)
- processed_order_lines.append(new_line)
- # in place modification of the sale order line in the list
- line_list[idx] = new_line
- return order
diff --git a/__unported__/connector_ecommerce/wizard/__init__.py b/__unported__/connector_ecommerce/wizard/__init__.py
deleted file mode 100644
index f5c43f15..00000000
--- a/__unported__/connector_ecommerce/wizard/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import sale_ignore_cancel
diff --git a/connector_ecommerce/README.rst b/connector_ecommerce/README.rst
new file mode 100644
index 00000000..56e5087a
--- /dev/null
+++ b/connector_ecommerce/README.rst
@@ -0,0 +1,66 @@
+.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
+ :alt: License
+
+Connector for E-Commerce
+========================
+
+This modules aims to be a common layer for the connectors dealing with
+e-commerce.
+
+It sits on top of the `connector`_ framework and is used by the
+e-commerce connectors, like `magentoerpconnect`_ or
+`prestashoperpconnect`_.
+
+That's a technical module, which include amongst other things:
+
+Events
+
+ On which the connectors can subscribe consumers
+ (tracking number added, invoice paid, picking sent, ...)
+
+ConnectorUnit
+
+ A piece of code which allows to play all the ``onchanges`` required
+ when we create a sales order.
+
+ Another one which allows to add special lines in imported sales orders
+ such as Shipping fees, Cash on Delivery or Discounts.
+
+Data Model
+
+ Add structures shared for e-commerce connectors
+
+.. _`connector`: http://odoo-connector.com
+.. _`magentoerpconnect`: http://odoo-magento-connector.com
+.. _`prestashoperpconnect`: https://github.com/OCA/connector-prestashop
+
+Installation
+============
+
+This module is a dependency for more advanced connectors. It does
+nothing on its own and there is no reason to install it alone.
+
+Credits
+=======
+
+Contributors
+------------
+
+See `contributors' list`_
+
+.. _contributors' list: ./AUTHORS
+
+Maintainer
+----------
+
+.. image:: http://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: http://odoo-community.org
+
+This module is maintained by the OCA.
+
+OCA, or the Odoo Community Association, is a nonprofit organization
+whose mission is to support the collaborative development of Odoo
+features and promote its widespread use.
+
+To contribute to this module, please visit http://odoo-community.org.
diff --git a/__unported__/connector_ecommerce/__init__.py b/connector_ecommerce/__init__.py
similarity index 82%
rename from __unported__/connector_ecommerce/__init__.py
rename to connector_ecommerce/__init__.py
index bac99af0..8a51dd0b 100644
--- a/__unported__/connector_ecommerce/__init__.py
+++ b/connector_ecommerce/__init__.py
@@ -19,12 +19,12 @@
#
##############################################################################
-import stock
-import account
-import product
-import invoice
-import payment_method
-import event
-import unit
-import sale
-import wizard
+from . import stock
+from . import account
+from . import product
+from . import invoice
+from . import payment_method
+from . import event
+from . import unit
+from . import sale
+from . import wizard
diff --git a/__unported__/connector_ecommerce/__openerp__.py b/connector_ecommerce/__openerp__.py
similarity index 58%
rename from __unported__/connector_ecommerce/__openerp__.py
rename to connector_ecommerce/__openerp__.py
index a0ef9380..172ee88e 100644
--- a/__unported__/connector_ecommerce/__openerp__.py
+++ b/connector_ecommerce/__openerp__.py
@@ -20,47 +20,14 @@
##############################################################################
{'name': 'Connector for E-Commerce',
- 'version': '2.2.0',
- 'category': 'Connector',
- 'author': "Connector Core Editors,Odoo Community Association (OCA)",
+ 'version': '3.0.0',
+ 'category': 'Hidden',
+ 'author': "Camptocamp,Akretion,Odoo Community Association (OCA)",
'website': 'http://openerp-connector.com',
'license': 'AGPL-3',
- 'description': """
-Connector for E-Commerce
-========================
-
-This modules aims to be a common layer for the connectors dealing with
-e-commerce.
-
-It sits on top of the `connector`_ framework and is used by the
-e-commerce connectors, like `magentoerpconnect`_ or
-`prestashoperpconnect`_.
-
-That's a technical module, which include amongst other things:
-
-Events
-
- On which the connectors can subscribe consumers
- (tracking number added, invoice paid, picking sent, ...)
-
-
-ConnectorUnit
-
- A piece of code which allows to play all the ``onchanges`` required
- when we create a sale order.
-
-Data Model
-
- Add structures shared for e-commerce connectors
-
-
- .. _`connector`: http://openerp-connector.com
-.. _`magentoerpconnect`: http://openerp-magento-connector.com
-.. _`prestashoperpconnect`: https://launchpad.net/prestashoperpconnect
-""",
'depends': [
'connector',
- 'sale_automatic_workflow',
+ 'sale_payment_method_automatic_workflow',
'sale_exceptions',
'delivery',
'connector_base_product',
@@ -76,5 +43,5 @@
'payment_method_view.xml',
'account_view.xml',
],
- 'installable': False,
+ 'installable': True,
}
diff --git a/__unported__/connector_ecommerce/account.py b/connector_ecommerce/account.py
similarity index 100%
rename from __unported__/connector_ecommerce/account.py
rename to connector_ecommerce/account.py
diff --git a/__unported__/connector_ecommerce/account_view.xml b/connector_ecommerce/account_view.xml
similarity index 100%
rename from __unported__/connector_ecommerce/account_view.xml
rename to connector_ecommerce/account_view.xml
diff --git a/__unported__/connector_ecommerce/ecommerce_data.xml b/connector_ecommerce/ecommerce_data.xml
similarity index 100%
rename from __unported__/connector_ecommerce/ecommerce_data.xml
rename to connector_ecommerce/ecommerce_data.xml
diff --git a/__unported__/connector_ecommerce/event.py b/connector_ecommerce/event.py
similarity index 100%
rename from __unported__/connector_ecommerce/event.py
rename to connector_ecommerce/event.py
diff --git a/__unported__/connector_ecommerce/i18n/connector_ecommerce.pot b/connector_ecommerce/i18n/connector_ecommerce.pot
similarity index 96%
rename from __unported__/connector_ecommerce/i18n/connector_ecommerce.pot
rename to connector_ecommerce/i18n/connector_ecommerce.pot
index f2f7538d..09671056 100644
--- a/__unported__/connector_ecommerce/i18n/connector_ecommerce.pot
+++ b/connector_ecommerce/i18n/connector_ecommerce.pot
@@ -163,7 +163,7 @@ msgstr ""
#. module: connector_ecommerce
#: help:product.template,tax_group_id:0
-msgid "Tax group are used with some external system like magento or prestashop"
+msgid "Tax groups are used with some external system like magento or prestashop"
msgstr ""
#. module: connector_ecommerce
@@ -173,7 +173,7 @@ msgstr ""
#. module: connector_ecommerce
#: field:sale.order,parent_need_cancel:0
-msgid "A parent sales orders needs cancel"
+msgid "A parent sales order needs cancel"
msgstr ""
#. module: connector_ecommerce
@@ -215,7 +215,7 @@ msgstr ""
#. module: connector_ecommerce
#: help:sale.order,parent_need_cancel:0
-msgid "A parent sales orders has been canceled on the backend and needs to be canceled."
+msgid "A parent sales order has been canceled on the backend and needs to be canceled."
msgstr ""
#. module: connector_ecommerce
@@ -251,7 +251,7 @@ msgstr ""
#. module: connector_ecommerce
#: help:payment.method,days_before_cancel:0
-msgid "After 'n' days, if the 'Import Rule' is not fulfilled, the import of the sale order will be canceled."
+msgid "After 'n' days, if the 'Import Rule' is not fulfilled, the import of the sales order will be canceled."
msgstr ""
#. module: connector_ecommerce
@@ -281,7 +281,7 @@ msgstr ""
#. module: connector_ecommerce
#: help:account.invoice,sale_order_ids:0
-msgid "This is the list of sale orders related to this invoice."
+msgid "This is the list of sales orders related to this invoice."
msgstr ""
#. module: connector_ecommerce
@@ -329,7 +329,7 @@ msgstr ""
#. module: connector_ecommerce
#: field:account.invoice,sale_order_ids:0
-msgid "Sale Orders"
+msgid "Sales Orders"
msgstr ""
#. module: connector_ecommerce
diff --git a/__unported__/connector_ecommerce/i18n/de.po b/connector_ecommerce/i18n/de.po
similarity index 97%
rename from __unported__/connector_ecommerce/i18n/de.po
rename to connector_ecommerce/i18n/de.po
index 40d89a8c..b9e01765 100644
--- a/__unported__/connector_ecommerce/i18n/de.po
+++ b/connector_ecommerce/i18n/de.po
@@ -185,7 +185,7 @@ msgstr "Weitere Informationen"
#. module: connector_ecommerce
#: help:product.template,tax_group_id:0
msgid ""
-"Tax group are used with some external system like magento or prestashop"
+"Tax groups are used with some external system like magento or prestashop"
msgstr ""
"Steuergruppen werden für einige externe Systeme wie Magento oder Prestashop "
"genutzt."
@@ -197,7 +197,7 @@ msgstr "Preislistenartikel"
#. module: connector_ecommerce
#: field:sale.order,parent_need_cancel:0
-msgid "A parent sales orders needs cancel"
+msgid "A parent sales order needs cancel"
msgstr "Eine übergeordnete"
#. module: connector_ecommerce
@@ -246,7 +246,7 @@ msgstr ""
#. module: connector_ecommerce
#: help:sale.order,parent_need_cancel:0
msgid ""
-"A parent sales orders has been canceled on the backend and needs to be "
+"A parent sales order has been canceled on the backend and needs to be "
"canceled."
msgstr ""
"Ein übergeordneter VK-Auftrag wurde im Backend abgebrochen und muss "
@@ -287,7 +287,7 @@ msgstr "VK-Auftrag"
#: help:payment.method,days_before_cancel:0
msgid ""
"After 'n' days, if the 'Import Rule' is not fulfilled, the import of the "
-"sale order will be canceled."
+"sales order will be canceled."
msgstr ""
"Nach 'n' Tagen, wenn die 'Import Regel' nicht erfüllt ist, wird der Import "
"abgebrochen."
@@ -319,7 +319,7 @@ msgstr "Im Backend abgebrochen"
#. module: connector_ecommerce
#: help:account.invoice,sale_order_ids:0
-msgid "This is the list of sale orders related to this invoice."
+msgid "This is the list of sales orders related to this invoice."
msgstr ""
"Dies ist die Liste der VK-Aufträge welche mit dieser Rechnung in beziehung "
"stehen."
@@ -369,7 +369,7 @@ msgstr "Name"
#. module: connector_ecommerce
#: field:account.invoice,sale_order_ids:0
-msgid "Sale Orders"
+msgid "Sales Orders"
msgstr "VK-Aufträge"
#. module: connector_ecommerce
diff --git a/__unported__/connector_ecommerce/i18n/es.po b/connector_ecommerce/i18n/es.po
similarity index 97%
rename from __unported__/connector_ecommerce/i18n/es.po
rename to connector_ecommerce/i18n/es.po
index 6a476ce3..3701bc7f 100644
--- a/__unported__/connector_ecommerce/i18n/es.po
+++ b/connector_ecommerce/i18n/es.po
@@ -186,7 +186,7 @@ msgstr "Otra información"
#. module: connector_ecommerce
#: help:product.template,tax_group_id:0
msgid ""
-"Tax group are used with some external system like magento or prestashop"
+"Tax groups are used with some external system like magento or prestashop"
msgstr ""
"El grupo fiscal se usa con algún sistema externo como magento do prestashop"
@@ -197,7 +197,7 @@ msgstr "Listado de Precios de Artículos"
#. module: connector_ecommerce
#: field:sale.order,parent_need_cancel:0
-msgid "A parent sales orders needs cancel"
+msgid "A parent sales order needs cancel"
msgstr "Una orden de venta padre debe cancelar"
#. module: connector_ecommerce
@@ -248,7 +248,7 @@ msgstr "Debe ser cancelado"
#. module: connector_ecommerce
#: help:sale.order,parent_need_cancel:0
msgid ""
-"A parent sales orders has been canceled on the backend and needs to be "
+"A parent sales order has been canceled on the backend and needs to be "
"canceled."
msgstr ""
"Una orden de venta padre ha sido cancelada en el área de administración y "
@@ -289,7 +289,7 @@ msgstr "Pedidos de venta"
#: help:payment.method,days_before_cancel:0
msgid ""
"After 'n' days, if the 'Import Rule' is not fulfilled, the import of the "
-"sale order will be canceled."
+"sales order will be canceled."
msgstr ""
"Después de 'n' días, si la 'Regla de Importación' no está realizada, la "
"importación de la orden de venta será cancelada."
@@ -321,7 +321,7 @@ msgstr "Cancelado en el área de administración"
#. module: connector_ecommerce
#: help:account.invoice,sale_order_ids:0
-msgid "This is the list of sale orders related to this invoice."
+msgid "This is the list of sales orders related to this invoice."
msgstr "Esta es la lista de pedidos de venta relacionados con esta factura."
#. module: connector_ecommerce
@@ -370,7 +370,7 @@ msgstr "Nombre"
#. module: connector_ecommerce
#: field:account.invoice,sale_order_ids:0
-msgid "Sale Orders"
+msgid "Sales Orders"
msgstr "Pedido de Venta"
#. module: connector_ecommerce
diff --git a/__unported__/connector_ecommerce/i18n/fr.po b/connector_ecommerce/i18n/fr.po
similarity index 97%
rename from __unported__/connector_ecommerce/i18n/fr.po
rename to connector_ecommerce/i18n/fr.po
index 19357ee2..7c98b831 100644
--- a/__unported__/connector_ecommerce/i18n/fr.po
+++ b/connector_ecommerce/i18n/fr.po
@@ -184,7 +184,7 @@ msgstr "Autres informations"
#. module: connector_ecommerce
#: help:product.template,tax_group_id:0
msgid ""
-"Tax group are used with some external system like magento or prestashop"
+"Tax groups are used with some external system like magento or prestashop"
msgstr ""
"Les groupes de taxes sont utilisés avec des systèmes externes comme Magento "
"ou Prestashop."
@@ -196,7 +196,7 @@ msgstr "Lignes de liste de prix"
#. module: connector_ecommerce
#: field:sale.order,parent_need_cancel:0
-msgid "A parent sales orders needs cancel"
+msgid "A parent sales order needs cancel"
msgstr "Une commande parente doit être annulée"
#. module: connector_ecommerce
@@ -248,7 +248,7 @@ msgstr "Doit être annulée"
#. module: connector_ecommerce
#: help:sale.order,parent_need_cancel:0
msgid ""
-"A parent sales orders has been canceled on the backend and needs to be "
+"A parent sales order has been canceled on the backend and needs to be "
"canceled."
msgstr ""
"Une commande parente a été annulée sur le backend et doit être annulée."
@@ -288,7 +288,7 @@ msgstr "Bon de commande"
#: help:payment.method,days_before_cancel:0
msgid ""
"After 'n' days, if the 'Import Rule' is not fulfilled, the import of the "
-"sale order will be canceled."
+"sales order will be canceled."
msgstr ""
"Après \"n\" jours, si la règle d'import n'est pas satisfaite, l'import de la "
"commande sera annulé."
@@ -320,7 +320,7 @@ msgstr "Annulée dans le backend"
#. module: connector_ecommerce
#: help:account.invoice,sale_order_ids:0
-msgid "This is the list of sale orders related to this invoice."
+msgid "This is the list of sales orders related to this invoice."
msgstr "Liste des commandes liées à cette facture."
#. module: connector_ecommerce
@@ -368,7 +368,7 @@ msgstr "Nom"
#. module: connector_ecommerce
#: field:account.invoice,sale_order_ids:0
-msgid "Sale Orders"
+msgid "Sales Orders"
msgstr "Bons de commande"
#. module: connector_ecommerce
diff --git a/__unported__/connector_ecommerce/i18n/nl.po b/connector_ecommerce/i18n/nl.po
similarity index 97%
rename from __unported__/connector_ecommerce/i18n/nl.po
rename to connector_ecommerce/i18n/nl.po
index 67153249..b05be271 100644
--- a/__unported__/connector_ecommerce/i18n/nl.po
+++ b/connector_ecommerce/i18n/nl.po
@@ -183,7 +183,7 @@ msgstr "Overige informatie"
#. module: connector_ecommerce
#: help:product.template,tax_group_id:0
msgid ""
-"Tax group are used with some external system like magento or prestashop"
+"Tax groups are used with some external system like magento or prestashop"
msgstr ""
"BTW groepen worden gebruikt met sommige externe systemen, zoals Magento of "
"Prestashop."
@@ -195,7 +195,7 @@ msgstr "Prijslijst items"
#. module: connector_ecommerce
#: field:sale.order,parent_need_cancel:0
-msgid "A parent sales orders needs cancel"
+msgid "A parent sales order needs cancel"
msgstr "Een bovenliggende verkooporder dient te worden geannuleerd"
#. module: connector_ecommerce
@@ -246,7 +246,7 @@ msgstr "Dient te worden geannuleerd"
#. module: connector_ecommerce
#: help:sale.order,parent_need_cancel:0
msgid ""
-"A parent sales orders has been canceled on the backend and needs to be "
+"A parent sales order has been canceled on the backend and needs to be "
"canceled."
msgstr ""
"Een bovenliggende verkooporder is geannuleerd in de backend en dient te "
@@ -287,7 +287,7 @@ msgstr "Verkooporder"
#: help:payment.method,days_before_cancel:0
msgid ""
"After 'n' days, if the 'Import Rule' is not fulfilled, the import of the "
-"sale order will be canceled."
+"sales order will be canceled."
msgstr ""
"Als na 'n' dagen niet aan de 'import regel' is voldaan, wordt de import van "
"de verkooporder geannuleerd."
@@ -319,7 +319,7 @@ msgstr "Geannuleerd in backend"
#. module: connector_ecommerce
#: help:account.invoice,sale_order_ids:0
-msgid "This is the list of sale orders related to this invoice."
+msgid "This is the list of sales orders related to this invoice."
msgstr ""
"Dit is een lijst met alle verkooporders gerelateerd aan deze factuur."
@@ -370,7 +370,7 @@ msgstr "Naam"
#. module: connector_ecommerce
#: field:account.invoice,sale_order_ids:0
-msgid "Sale Orders"
+msgid "Sales Orders"
msgstr "Verkooporders"
#. module: connector_ecommerce
diff --git a/__unported__/connector_ecommerce/invoice.py b/connector_ecommerce/invoice.py
similarity index 57%
rename from __unported__/connector_ecommerce/invoice.py
rename to connector_ecommerce/invoice.py
index 0d72c44a..56c67572 100644
--- a/__unported__/connector_ecommerce/invoice.py
+++ b/connector_ecommerce/invoice.py
@@ -19,37 +19,28 @@
#
##############################################################################
-from openerp.osv import fields, orm
+from openerp import models, api
from openerp.addons.connector.session import ConnectorSession
from .event import on_invoice_paid, on_invoice_validated
-class account_invoice(orm.Model):
+class AccountInvoice(models.Model):
_inherit = 'account.invoice'
- _columns = {
- 'sale_order_ids': fields.many2many(
- # TODO duplicate with 'sale_ids', replace
- 'sale.order',
- 'sale_order_invoice_rel',
- 'invoice_id',
- 'order_id',
- string='Sale Orders', readonly=True,
- help="This is the list of sale orders related to this invoice."),
- }
-
- def confirm_paid(self, cr, uid, ids, context=None):
- res = super(account_invoice, self).confirm_paid(
- cr, uid, ids, context=context)
- session = ConnectorSession(cr, uid, context=context)
- for record_id in ids:
+ @api.multi
+ def confirm_paid(self):
+ res = super(AccountInvoice, self).confirm_paid()
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ for record_id in self.ids:
on_invoice_paid.fire(session, self._name, record_id)
return res
- def invoice_validate(self, cr, uid, ids, context=None):
- res = super(account_invoice, self).invoice_validate(
- cr, uid, ids, context=context)
- session = ConnectorSession(cr, uid, context=context)
- for record_id in ids:
+ @api.multi
+ def invoice_validate(self):
+ res = super(AccountInvoice, self).invoice_validate()
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ for record_id in self.ids:
on_invoice_validated.fire(session, self._name, record_id)
return res
diff --git a/__unported__/connector_ecommerce/invoice_view.xml b/connector_ecommerce/invoice_view.xml
similarity index 100%
rename from __unported__/connector_ecommerce/invoice_view.xml
rename to connector_ecommerce/invoice_view.xml
diff --git a/connector_ecommerce/payment_method.py b/connector_ecommerce/payment_method.py
new file mode 100644
index 00000000..028b19bd
--- /dev/null
+++ b/connector_ecommerce/payment_method.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright 2011-2013 Akretion
+# @author Sébastien BEAU
+#
+# 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 .
+#
+##############################################################################
+
+from openerp import models, fields, api
+
+
+class PaymentMethod(models.Model):
+ _inherit = "payment.method"
+
+ @api.model
+ def _get_import_rules(self):
+ return [('always', 'Always'),
+ ('never', 'Never'),
+ ('paid', 'Paid'),
+ ('authorized', 'Authorized'),
+ ]
+
+ # the logic around the 2 following fields has to be implemented
+ # in the connectors (magentoerpconnect, prestashoperpconnect,...)
+ days_before_cancel = fields.Integer(
+ string='Days before cancel',
+ default=30,
+ help="After 'n' days, if the 'Import Rule' is not fulfilled, the "
+ "import of the sales order will be canceled.",
+ )
+ import_rule = fields.Selection(selection='_get_import_rules',
+ string="Import Rule",
+ default='always',
+ required=True)
+
+ @api.model
+ def get_or_create_payment_method(self, payment_method):
+ """ Try to get a payment method or create if it doesn't exist
+
+ :param payment_method: payment method like PayPal, etc.
+ :type payment_method: str
+ :return: required payment method
+ :rtype: recordset
+ """
+ domain = [('name', '=ilike', payment_method)]
+ method = self.search(domain, limit=1)
+ if not method:
+ method = self.create({'name': payment_method})
+ return method
diff --git a/__unported__/connector_ecommerce/payment_method_view.xml b/connector_ecommerce/payment_method_view.xml
similarity index 91%
rename from __unported__/connector_ecommerce/payment_method_view.xml
rename to connector_ecommerce/payment_method_view.xml
index 788b3bd3..a6ef4428 100644
--- a/__unported__/connector_ecommerce/payment_method_view.xml
+++ b/connector_ecommerce/payment_method_view.xml
@@ -12,7 +12,7 @@
payment.method.connector_ecommerce.form
payment.method
-
+
@@ -40,7 +40,7 @@
payment.method.connector_ecommerce.tree
payment.method
-
+
diff --git a/connector_ecommerce/product.py b/connector_ecommerce/product.py
new file mode 100644
index 00000000..7abb6fa8
--- /dev/null
+++ b/connector_ecommerce/product.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Sébastien BEAU
+# Copyright 2011-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 .
+#
+##############################################################################
+
+from openerp import models, fields, api
+from openerp.addons.connector.session import ConnectorSession
+from .event import on_product_price_changed
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ # TODO implement set function and also support multi tax
+ @api.one
+ @api.depends('taxes_id', 'taxes_id.group_id')
+ def _get_tax_group_id(self):
+ taxes = self.taxes_id
+ self.tax_group_id = taxes[0].group_id.id if taxes else False
+
+ tax_group_id = fields.Many2one(
+ comodel_name='account.tax.group',
+ compute='_get_tax_group_id',
+ string='Tax Group',
+ help='Tax groups are used with some external '
+ 'system like Prestashop',
+ )
+
+ @api.multi
+ def _price_changed(self, vals):
+ """ Fire the ``on_product_price_changed`` on all the variants of
+ the template if the price of the product could have changed.
+
+ If one of the field used in a sale pricelist item has been
+ modified, we consider that the price could have changed.
+
+ There is no guarantee that's the price actually changed,
+ because it depends on the pricelists.
+ """
+ type_model = self.env['product.price.type']
+ price_fields = type_model.sale_price_fields()
+ # restrict the fields to the template ones only, so if
+ # the write has been done on product.product, we won't
+ # update all the variants if a price field of the
+ # variant has been changed
+ tmpl_fields = [field for field in vals if field in self._fields]
+ if any(field in price_fields for field in tmpl_fields):
+ product_model = self.env['product.product']
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ products = product_model.search(
+ [('product_tmpl_id', 'in', self.ids)]
+ )
+ # when the write is done on the product.product, avoid
+ # to fire the event 2 times
+ if self.env.context.get('from_product_ids'):
+ from_product_ids = self.env.context['from_product_ids']
+ remove_products = product_model.browse(from_product_ids)
+ products -= remove_products
+ for product in products:
+ on_product_price_changed.fire(session,
+ product_model._name,
+ product.id)
+
+ @api.multi
+ def write(self, vals):
+ result = super(ProductTemplate, self).write(vals)
+ self._price_changed(vals)
+ return result
+
+
+class ProductProduct(models.Model):
+ _inherit = 'product.product'
+
+ @api.depends()
+ def _get_checkpoint(self):
+ checkpoint_model = self.env['connector.checkpoint']
+ model_model = self.env['ir.model']
+ model = model_model.search([('model', '=', 'product.product')])
+ for product in self:
+ points = checkpoint_model.search([('model_id', '=', model.id),
+ ('record_id', '=', product.id),
+ ('state', '=', 'need_review')],
+ limit=1,
+ )
+ product.has_checkpoint = bool(points)
+
+ has_checkpoint = fields.Boolean(compute='_get_checkpoint',
+ string='Has Checkpoint')
+
+ @api.multi
+ def _price_changed(self, vals):
+ """ Fire the ``on_product_price_changed`` if the price
+ of the product could have changed.
+
+ If one of the field used in a sale pricelist item has been
+ modified, we consider that the price could have changed.
+
+ There is no guarantee that's the price actually changed,
+ because it depends on the pricelists.
+ """
+ type_model = self.env['product.price.type']
+ price_fields = type_model.sale_price_fields()
+ if any(field in price_fields for field in vals):
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ for prod_id in self.ids:
+ on_product_price_changed.fire(session, self._name, prod_id)
+
+ @api.multi
+ def write(self, vals):
+ self_context = self.with_context(from_product_ids=self.ids)
+ result = super(ProductProduct, self_context).write(vals)
+ self._price_changed(vals)
+ return result
+
+ @api.model
+ def create(self, vals):
+ product = super(ProductProduct, self).create(vals)
+ self._price_changed(vals)
+ return product
+
+
+class ProductPriceType(models.Model):
+ _inherit = 'product.price.type'
+
+ pricelist_item_ids = fields.One2many(
+ comodel_name='product.pricelist.item',
+ inverse_name='base',
+ string='Pricelist Items',
+ readonly=True,
+ )
+
+ @api.model
+ def sale_price_fields(self):
+ """ Returns a list of fields used by sale pricelists.
+ Used to know if the sale price could have changed
+ when one of these fields has changed.
+ """
+ item_model = self.env['product.pricelist.item']
+ items = item_model.search(
+ [('price_version_id.pricelist_id.type', '=', 'sale')],
+ )
+ types = self.search([('pricelist_item_ids', 'in', items.ids)])
+ return [t.field for t in types]
diff --git a/connector_ecommerce/sale.py b/connector_ecommerce/sale.py
new file mode 100644
index 00000000..e8fe1b93
--- /dev/null
+++ b/connector_ecommerce/sale.py
@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Guewen Baconnier
+# Copyright 2011-2013 Camptocamp SA
+# Author: Sébastien Beau
+# Copyright 2010-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 .
+#
+##############################################################################
+
+import logging
+
+from openerp import models, fields, api, exceptions, _, osv
+from openerp.addons.connector.connector import ConnectorUnit
+
+_logger = logging.getLogger(__name__)
+
+
+class SaleOrder(models.Model):
+ """ Add a cancellation mecanism in the sales orders
+
+ When a sales order is canceled in a backend, the connectors can flag
+ the 'canceled_in_backend' flag. It will:
+
+ * try to automatically cancel the sales order
+ * block the confirmation of the sales orders using a 'sales exception'
+
+ When a sales order is canceled or the user used the button to force
+ to 'keep it open', the flag 'cancellation_resolved' is set to True.
+
+ The second axe which can be used by the connectors is the 'parent'
+ sales order. When a sales order has a parent sales order (logic to
+ link with the parent to be defined by each connector), it will be
+ blocked until the cancellation of the sales order is resolved.
+
+ This is used by, for instance, the magento connector, when one
+ modifies a sales order, Magento cancels it and create a new one with
+ the first one as parent.
+ """
+ _inherit = 'sale.order'
+
+ canceled_in_backend = fields.Boolean(string='Canceled in backend',
+ readonly=True)
+ # set to True when the cancellation from the backend is
+ # resolved, either because the SO has been canceled or
+ # because the user manually chose to keep it open
+ cancellation_resolved = fields.Boolean(string='Cancellation from the '
+ 'backend resolved')
+ parent_id = fields.Many2one(comodel_name='sale.order',
+ compute='get_parent_id',
+ string='Parent Order',
+ help='A parent sales order is a sales '
+ 'order replaced by this one.')
+ need_cancel = fields.Boolean(compute='_need_cancel',
+ string='Need to be canceled',
+ help='Has been canceled on the backend'
+ ', need to be canceled.')
+ parent_need_cancel = fields.Boolean(
+ compute='_parent_need_cancel',
+ string='A parent sales order needs cancel',
+ help='A parent sales order has been canceled on the backend'
+ ' and needs to be canceled.',
+ )
+
+ @api.one
+ @api.depends()
+ def get_parent_id(self):
+ """ Need to be inherited in the connectors to implement the
+ parent logic.
+
+ See an implementation example in ``magentoerpconnect``.
+ """
+ self.parent_id = False
+
+ @api.one
+ @api.depends('canceled_in_backend', 'cancellation_resolved')
+ def _need_cancel(self):
+ """ Return True if the sales order need to be canceled
+ (has been canceled on the Backend)
+ """
+ self.need_cancel = (self.canceled_in_backend and
+ not self.cancellation_resolved)
+
+ @api.one
+ @api.depends('need_cancel', 'parent_id',
+ 'parent_id.need_cancel', 'parent_id.parent_need_cancel')
+ def _parent_need_cancel(self):
+ """ Return True if at least one parent sales order need to
+ be canceled (has been canceled on the backend).
+ Follows all the parent sales orders.
+ """
+ self.parent_need_cancel = False
+ order = self.parent_id
+ while order:
+ if order.need_cancel:
+ self.parent_need_cancel = True
+ order = order.parent_id
+
+ @api.multi
+ def _try_auto_cancel(self):
+ """ Try to automatically cancel a sales order canceled
+ in a backend.
+
+ If it can't cancel it, does nothing.
+ """
+ wkf_states = ('draft', 'sent')
+ action_states = ('manual', 'progress')
+ resolution_msg = _("Resolution:
"
+ "- Cancel the linked invoices, delivery "
+ "orders, automatic payments.
"
+ "- Cancel the sales order manually.
"
+ "
")
+ for order in self:
+ state = order.state
+ if state == 'cancel':
+ continue
+ elif state == 'done':
+ message = _("The sales order cannot be automatically "
+ "canceled because it is already done.")
+ elif state in wkf_states + action_states:
+ try:
+ # respect the same cancellation methods than
+ # the sales order view: quotations use the workflow
+ # action, sales orders use the action_cancel method.
+ if state in wkf_states:
+ order.signal_workflow('cancel')
+ elif state in action_states:
+ order.action_cancel()
+ else:
+ raise ValueError('%s should not fall here.' % state)
+ except (osv.except_osv, osv.orm.except_orm,
+ exceptions.Warning):
+ # the 'cancellation_resolved' flag will stay to False
+ message = _("The sales order could not be automatically "
+ "canceled.") + resolution_msg
+ else:
+ message = _("The sales order has been automatically "
+ "canceled.")
+ else:
+ # shipping_except, invoice_except, ...
+ # can not be canceled from the view, so assume that it
+ # should not be canceled here neiter, exception to
+ # resolve
+ message = _("The sales order could not be automatically "
+ "canceled for this status.") + resolution_msg
+ order.message_post(body=message)
+
+ @api.multi
+ def _log_canceled_in_backend(self):
+ message = _("The sales order has been canceled on the backend.")
+ self.message_post(body=message)
+ for order in self:
+ message = _("Warning: the origin sales order %s has been canceled "
+ "on the backend.") % order.name
+ if order.picking_ids:
+ order.picking_ids.message_post(body=message)
+ if order.invoice_ids:
+ order.invoice_ids.message_post(body=message)
+
+ @api.model
+ def create(self, values):
+ order = super(SaleOrder, self).create(values)
+ if values.get('canceled_in_backend'):
+ order._log_canceled_in_backend()
+ order._try_auto_cancel()
+ return order
+
+ @api.multi
+ def write(self, values):
+ result = super(SaleOrder, self).write(values)
+ if values.get('canceled_in_backend'):
+ self._log_canceled_in_backend()
+ self._try_auto_cancel()
+ return result
+
+ @api.multi
+ def action_cancel(self):
+ res = super(SaleOrder, self).action_cancel()
+ for sale in self:
+ # the sales order is canceled => considered as resolved
+ if (sale.canceled_in_backend and
+ not sale.cancellation_resolved):
+ sale.write({'cancellation_resolved': True})
+ return res
+
+ @api.multi
+ def ignore_cancellation(self, reason):
+ """ Manually set the cancellation from the backend as resolved.
+
+ The user can choose to keep the sales order active for some reason,
+ it only requires to push a button to keep it alive.
+ """
+ message = (_("Despite the cancellation of the sales order on the "
+ "backend, it should stay open.
Reason: %s") %
+ reason)
+ self.message_post(body=message)
+ self.write({'cancellation_resolved': True})
+ return True
+
+ @api.multi
+ def action_view_parent(self):
+ """ Return an action to display the parent sales order """
+ self.ensure_one()
+
+ parent = self.parent_id
+ if not parent:
+ return
+
+ view_xmlid = 'sale.view_order_form'
+ if parent.state in ('draft', 'sent', 'cancel'):
+ action_xmlid = 'sale.action_quotations'
+ else:
+ action_xmlid = 'sale.action_orders'
+
+ action = self.env.ref(action_xmlid).read()[0]
+
+ view = self.env.ref(view_xmlid)
+ action['views'] = [(view.id if view else False, 'form')]
+ action['res_id'] = parent.id
+ return action
+
+
+class SpecialOrderLineBuilder(ConnectorUnit):
+ """ Base class to build a sales order line for a sales order
+
+ Used when extra order lines have to be added in a sales order
+ but we only know some parameters (product, price, ...), for instance,
+ a line for the shipping costs or the gift coupons.
+
+ It can be subclassed to customize the way the lines are created.
+
+ Usage::
+
+ builder = self.get_connector_for_unit(ShippingLineBuilder,
+ model='sale.order.line')
+ builder.price_unit = 100
+ builder.get_line()
+
+ """
+ _model_name = None
+
+ def __init__(self, connector_env):
+ super(SpecialOrderLineBuilder, self).__init__(connector_env)
+ self.product = None # id or browse_record
+ # when no product_id, fallback to a product_ref
+ self.product_ref = None # tuple (module, xmlid)
+ self.price_unit = None
+ self.quantity = 1
+ self.sign = 1
+ self.sequence = 980
+
+ def get_line(self):
+ assert self.product_ref or self.product
+ assert self.price_unit is not None
+
+ product = self.product
+ if product is None:
+ product = self.env.ref('.'.join(self.product_ref))
+
+ if not isinstance(product, models.BaseModel):
+ product = self.env['product.product'].browse(product)
+ return {'product_id': product.id,
+ 'name': product.name,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': self.quantity,
+ 'price_unit': self.price_unit * self.sign,
+ 'sequence': self.sequence}
+
+
+class ShippingLineBuilder(SpecialOrderLineBuilder):
+ """ Return values for a Shipping line """
+ _model_name = None
+
+ def __init__(self, connector_env):
+ super(ShippingLineBuilder, self).__init__(connector_env)
+ self.product_ref = ('connector_ecommerce', 'product_product_shipping')
+ self.sequence = 999
+
+
+class CashOnDeliveryLineBuilder(SpecialOrderLineBuilder):
+ """ Return values for a Cash on Delivery line """
+ _model_name = None
+ _model_name = None
+
+ def __init__(self, connector_env):
+ super(CashOnDeliveryLineBuilder, self).__init__(connector_env)
+ self.product_ref = ('connector_ecommerce',
+ 'product_product_cash_on_delivery')
+ self.sequence = 995
+
+
+class GiftOrderLineBuilder(SpecialOrderLineBuilder):
+ """ Return values for a Gift line """
+ _model_name = None
+
+ def __init__(self, connector_env):
+ super(GiftOrderLineBuilder, self).__init__(connector_env)
+ self.product_ref = ('connector_ecommerce',
+ 'product_product_gift')
+ self.sign = -1
+ self.gift_code = None
+ self.sequence = 990
+
+ def get_line(self):
+ line = super(GiftOrderLineBuilder, self).get_line()
+ if self.gift_code:
+ line['name'] = "%s [%s]" % (line['name'], self.gift_code)
+ return line
diff --git a/__unported__/connector_ecommerce/sale_view.xml b/connector_ecommerce/sale_view.xml
similarity index 100%
rename from __unported__/connector_ecommerce/sale_view.xml
rename to connector_ecommerce/sale_view.xml
diff --git a/__unported__/connector_ecommerce/security/ir.model.access.csv b/connector_ecommerce/security/ir.model.access.csv
similarity index 100%
rename from __unported__/connector_ecommerce/security/ir.model.access.csv
rename to connector_ecommerce/security/ir.model.access.csv
diff --git a/__unported__/connector_ecommerce/security/security.xml b/connector_ecommerce/security/security.xml
similarity index 100%
rename from __unported__/connector_ecommerce/security/security.xml
rename to connector_ecommerce/security/security.xml
diff --git a/connector_ecommerce/stock.py b/connector_ecommerce/stock.py
new file mode 100644
index 00000000..f0132c79
--- /dev/null
+++ b/connector_ecommerce/stock.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Joel Grand-Guillaume
+# Copyright 2013-2015 Camptocamp SA
+#
+# 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 .
+#
+##############################################################################
+
+from openerp import models, fields, api
+
+from openerp.addons.connector.session import ConnectorSession
+from .event import on_picking_out_done, on_tracking_number_added
+
+
+class StockPicking(models.Model):
+ _inherit = 'stock.picking'
+
+ related_backorder_ids = fields.One2many(
+ comodel_name='stock.picking',
+ inverse_name='backorder_id',
+ string="Related backorders",
+ )
+
+ @api.multi
+ def write(self, vals):
+ res = super(StockPicking, self).write(vals)
+ if vals.get('carrier_tracking_ref'):
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ for record_id in self.ids:
+ on_tracking_number_added.fire(session, self._name, record_id)
+ return res
+
+ @api.multi
+ def do_transfer(self):
+ # The key in the context avoid the event to be fired in
+ # StockMove.action_done(). Allow to handle the partial pickings
+ self_context = self.with_context(__no_on_event_out_done=True)
+ result = super(StockPicking, self_context).do_transfer()
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ for picking in self:
+ if picking.picking_type_id.code != 'outgoing':
+ continue
+ if picking.related_backorder_ids:
+ method = 'partial'
+ else:
+ method = 'complete'
+ on_picking_out_done.fire(session, 'stock.picking',
+ picking.id, method)
+
+ return result
+
+
+class StockMove(models.Model):
+ _inherit = 'stock.move'
+
+ @api.multi
+ def action_done(self):
+ fire_event = not self.env.context.get('__no_on_event_out_done')
+ if fire_event:
+ pickings = self.mapped('picking_id')
+ states = {p.id: p.state for p in pickings}
+
+ result = super(StockMove, self).action_done()
+
+ if fire_event:
+ session = ConnectorSession(self.env.cr, self.env.uid,
+ context=self.env.context)
+ for picking in pickings:
+ if states[picking.id] != 'done' and picking.state == 'done':
+ if picking.picking_type_id.code != 'outgoing':
+ continue
+ # partial pickings are handled in
+ # StockPicking.do_transfer()
+ on_picking_out_done.fire(session, 'stock.picking',
+ picking.id, 'complete')
+
+ return result
diff --git a/connector_ecommerce/stock_view.xml b/connector_ecommerce/stock_view.xml
new file mode 100644
index 00000000..954c7050
--- /dev/null
+++ b/connector_ecommerce/stock_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ stock.picking.form
+ stock.picking
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__unported__/connector_ecommerce/tests/__init__.py b/connector_ecommerce/tests/__init__.py
similarity index 88%
rename from __unported__/connector_ecommerce/tests/__init__.py
rename to connector_ecommerce/tests/__init__.py
index 988e9d44..f514ea16 100644
--- a/__unported__/connector_ecommerce/tests/__init__.py
+++ b/connector_ecommerce/tests/__init__.py
@@ -19,13 +19,6 @@
#
##############################################################################
-import test_onchange
-import test_invoice_event
-
-fast_suite = [
-]
-
-checks = [
- test_onchange,
- test_invoice_event,
-]
+from . import test_onchange
+from . import test_invoice_event
+from . import test_picking_event
diff --git a/__unported__/connector_ecommerce/tests/test_invoice_event.py b/connector_ecommerce/tests/test_invoice_event.py
similarity index 61%
rename from __unported__/connector_ecommerce/tests/test_invoice_event.py
rename to connector_ecommerce/tests/test_invoice_event.py
index f5c7195a..9a240a55 100644
--- a/__unported__/connector_ecommerce/tests/test_invoice_event.py
+++ b/connector_ecommerce/tests/test_invoice_event.py
@@ -20,48 +20,39 @@
##############################################################################
import mock
-from functools import partial
import openerp.tests.common as common
-from openerp import netsvc
-class test_invoice_event(common.TransactionCase):
+class TestInvoiceEvent(common.TransactionCase):
""" Test if the events on the invoice are fired correctly """
def setUp(self):
- super(test_invoice_event, self).setUp()
- cr, uid = self.cr, self.uid
- self.invoice_model = self.registry('account.invoice')
- partner_model = self.registry('res.partner')
- partner_id = partner_model.create(cr, uid, {'name': 'Hodor'})
- data_model = self.registry('ir.model.data')
- self.get_ref = partial(data_model.get_object_reference, cr, uid)
- product_id = self.get_ref('product', 'product_product_6')[1]
- invoice_vals = {'partner_id': partner_id,
+ super(TestInvoiceEvent, self).setUp()
+ self.invoice_model = self.env['account.invoice']
+ partner_model = self.env['res.partner']
+ partner = partner_model.create({'name': 'Hodor'})
+ product = self.env.ref('product.product_product_6')
+ invoice_vals = {'partner_id': partner.id,
'type': 'out_invoice',
'invoice_line': [(0, 0, {'name': "LCD Screen",
- 'product_id': product_id,
+ 'product_id': product.id,
'quantity': 5,
'price_unit': 200})],
}
- onchange_res = self.invoice_model.onchange_partner_id(
- cr, uid, [], 'out_invoice', partner_id)
+ onchange_res = self.invoice_model.onchange_partner_id('out_invoice',
+ partner.id)
invoice_vals.update(onchange_res['value'])
- invoice_id = self.invoice_model.create(cr, uid, invoice_vals)
- self.invoice = self.invoice_model.browse(cr, uid, invoice_id)
+ self.invoice = self.invoice_model.create(invoice_vals)
def test_event_validated(self):
""" Test if the ``on_invoice_validated`` event is fired
when an invoice is validated """
- cr, uid = self.cr, self.uid
assert self.invoice, "The invoice has not been created"
- wf_service = netsvc.LocalService('workflow')
event = ('openerp.addons.connector_ecommerce.'
'invoice.on_invoice_validated')
with mock.patch(event) as event_mock:
- wf_service.trg_validate(uid, 'account.invoice',
- self.invoice.id, 'invoice_open', cr)
+ self.invoice.signal_workflow('invoice_open')
self.assertEqual(self.invoice.state, 'open')
event_mock.fire.assert_called_with(mock.ANY,
'account.invoice',
@@ -70,27 +61,23 @@ def test_event_validated(self):
def test_event_paid(self):
""" Test if the ``on_invoice_paid`` event is fired
when an invoice is paid """
- cr, uid = self.cr, self.uid
assert self.invoice, "The invoice has not been created"
- wf_service = netsvc.LocalService('workflow')
- wf_service.trg_validate(uid, 'account.invoice',
- self.invoice.id, 'invoice_open', cr)
+ self.invoice.signal_workflow('invoice_open')
self.assertEqual(self.invoice.state, 'open')
- journal_id = self.get_ref('account', 'bank_journal')[1]
- pay_account_id = self.get_ref('account', 'cash')[1]
- period_id = self.get_ref('account', 'period_10')[1]
+ journal = self.env.ref('account.bank_journal')
+ pay_account = self.env.ref('account.cash')
+ period = self.env.ref('account.period_10')
event = 'openerp.addons.connector_ecommerce.invoice.on_invoice_paid'
with mock.patch(event) as event_mock:
self.invoice.pay_and_reconcile(
pay_amount=self.invoice.amount_total,
- pay_account_id=pay_account_id,
- period_id=period_id,
- pay_journal_id=journal_id,
- writeoff_acc_id=pay_account_id,
- writeoff_period_id=period_id,
- writeoff_journal_id=journal_id,
+ pay_account_id=pay_account.id,
+ period_id=period.id,
+ pay_journal_id=journal.id,
+ writeoff_acc_id=pay_account.id,
+ writeoff_period_id=period.id,
+ writeoff_journal_id=journal.id,
name="Payment for test of the event on_invoice_paid")
- self.invoice.refresh()
self.assertEqual(self.invoice.state, 'paid')
event_mock.fire.assert_called_with(mock.ANY,
'account.invoice',
diff --git a/connector_ecommerce/tests/test_onchange.py b/connector_ecommerce/tests/test_onchange.py
new file mode 100644
index 00000000..bced71b0
--- /dev/null
+++ b/connector_ecommerce/tests/test_onchange.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+###############################################################################
+#
+# connector-ecommerce for OpenERP
+# Copyright (C) 2013-TODAY Akretion .
+# @author Sébastien BEAU
+#
+# 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 .
+#
+###############################################################################
+
+import mock
+
+from openerp.addons.connector_ecommerce.unit.sale_order_onchange import (
+ SaleOrderOnChange)
+from openerp.addons.connector.session import ConnectorSession
+from openerp.addons.connector.connector import Environment
+import openerp.tests.common as common
+
+DB = common.DB
+ADMIN_USER_ID = common.ADMIN_USER_ID
+
+
+class TestOnchange(common.TransactionCase):
+ """ Test if the onchanges are applied correctly on a sales order"""
+
+ def setUp(self):
+ super(TestOnchange, self).setUp()
+ self.session = ConnectorSession(self.cr, self.uid)
+
+ def test_play_onchange(self):
+ """ Play the onchange ConnectorUnit on a sales order """
+ product_model = self.env['product.product']
+ partner_model = self.env['res.partner']
+ tax_model = self.env['account.tax']
+ payment_method_model = self.env['payment.method']
+
+ backend_record = mock.Mock()
+ env = Environment(backend_record, self.session, 'sale.order')
+
+ partner = partner_model.create({'name': 'seb',
+ 'zip': '69100',
+ 'city': 'Villeurbanne'})
+ partner_invoice = partner_model.create({'name': 'Guewen',
+ 'zip': '1015',
+ 'city': 'Lausanne',
+ 'type': 'invoice',
+ 'parent_id': partner.id})
+ tax = tax_model.create({'name': 'My Tax'})
+ product = product_model.create({'default_code': 'MyCode',
+ 'name': 'My Product',
+ 'weight': 15,
+ 'taxes_id': [(6, 0, [tax.id])]})
+ payment_term = self.env.ref('account.account_payment_term_advance')
+ payment_method = payment_method_model.create({
+ 'name': 'Cash',
+ 'payment_term_id': payment_term.id,
+ })
+
+ order_vals = {
+ 'name': 'mag_10000001',
+ 'partner_id': partner.id,
+ 'payment_method_id': payment_method.id,
+ 'order_line': [
+ (0, 0, {'product_id': product.id,
+ 'price_unit': 20,
+ 'name': 'My Real Name',
+ 'product_uom_qty': 1,
+ 'sequence': 1,
+ }
+ ),
+ ],
+ # fake field for the lines coming from a backend
+ 'backend_order_line': [
+ (0, 0, {'product_id': product.id,
+ 'price_unit': 10,
+ 'name': 'Line 2',
+ 'product_uom_qty': 2,
+ 'sequence': 2,
+ }
+ ),
+ ],
+ }
+
+ extra_lines = order_vals['backend_order_line']
+
+ onchange = SaleOrderOnChange(env)
+ order = onchange.play(order_vals, extra_lines)
+
+ self.assertEqual(order['partner_invoice_id'], partner_invoice.id)
+ self.assertEqual(order['payment_term'], payment_term.id)
+ self.assertEqual(len(order['order_line']), 1)
+ line = order['order_line'][0][2]
+ self.assertEqual(line['name'], 'My Real Name')
+ self.assertEqual(line['th_weight'], 15)
+ self.assertEqual(line['tax_id'], [(6, 0, [tax.id])])
+ line = order['backend_order_line'][0][2]
+ self.assertEqual(line['name'], 'Line 2')
+ self.assertEqual(line['th_weight'], 30)
+ self.assertEqual(line['tax_id'], [(6, 0, [tax.id])])
diff --git a/connector_ecommerce/tests/test_picking_event.py b/connector_ecommerce/tests/test_picking_event.py
new file mode 100644
index 00000000..b4d03cfc
--- /dev/null
+++ b/connector_ecommerce/tests/test_picking_event.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Guewen Baconnier
+# Copyright 2015 Camptocamp SA
+#
+# 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 .
+#
+##############################################################################
+
+import mock
+
+import openerp.tests.common as common
+
+
+class TestPickingEvent(common.TransactionCase):
+ """ Test if the events on the pickings are fired correctly """
+
+ def setUp(self):
+ super(TestPickingEvent, self).setUp()
+ self.picking_model = self.env['stock.picking']
+ self.sale_model = self.env['sale.order']
+ self.sale_line_model = self.env['sale.order.line']
+
+ partner_model = self.env['res.partner']
+ partner = partner_model.create({'name': 'Benjy'})
+ self.sale = self.sale_model.create({'partner_id': partner.id})
+ self.sale_line_model.create({
+ 'order_id': self.sale.id,
+ 'product_id': self.env.ref('product.product_product_33').id,
+ 'name': "[HEAD-USB] Headset USB",
+ 'product_uom_qty': 42,
+ 'product_uom': self.env.ref('product.product_uom_unit').id,
+ 'price_unit': 65,
+ })
+ self.sale_line_model.create({
+ 'order_id': self.sale.id,
+ 'product_id': self.env.ref('product.product_product_28').id,
+ 'name': "[EXT-HDD] External Hard disk",
+ 'product_uom_qty': 2,
+ 'product_uom': self.env.ref('product.product_uom_unit').id,
+ 'price_unit': 405,
+ })
+ self.sale.signal_workflow('order_confirm')
+ self.picking = self.sale.picking_ids
+
+ def test_event_on_picking_out_done(self):
+ """ Test if the ``on_picking_out_done`` event is fired
+ when an outgoing picking is done """
+ self.picking.force_assign()
+ event = ('openerp.addons.connector_ecommerce.'
+ 'stock.on_picking_out_done')
+ with mock.patch(event) as event_mock:
+ self.picking.action_done()
+ self.assertEquals(self.picking.state, 'done')
+ event_mock.fire.assert_called_with(mock.ANY,
+ 'stock.picking',
+ self.picking.id,
+ 'complete')
+
+ def test_event_on_picking_out_done_partial(self):
+ """ Test if the ``on_picking_out_done`` informs of the partial
+ pickings """
+ self.picking.force_assign()
+ self.picking.do_prepare_partial()
+ for operation in self.picking.pack_operation_ids:
+ operation.product_qty = 1
+ event = ('openerp.addons.connector_ecommerce.'
+ 'stock.on_picking_out_done')
+ with mock.patch(event) as event_mock:
+ self.picking.do_transfer()
+ self.assertEquals(self.picking.state, 'done')
+ event_mock.fire.assert_called_with(mock.ANY,
+ 'stock.picking',
+ self.picking.id,
+ 'partial')
+
+ def test_event_on_tracking_number_added(self):
+ """ Test if the ``on_tracking_number_added`` event is fired
+ when a tracking number is added """
+ event = ('openerp.addons.connector_ecommerce.'
+ 'stock.on_tracking_number_added')
+ with mock.patch(event) as event_mock:
+ self.picking.carrier_tracking_ref = 'XYZ'
+ event_mock.fire.assert_called_with(mock.ANY,
+ 'stock.picking',
+ self.picking.id)
diff --git a/connector_ecommerce/unit/__init__.py b/connector_ecommerce/unit/__init__.py
new file mode 100644
index 00000000..45443ad8
--- /dev/null
+++ b/connector_ecommerce/unit/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import sale_order_onchange
diff --git a/connector_ecommerce/unit/sale_order_onchange.py b/connector_ecommerce/unit/sale_order_onchange.py
new file mode 100644
index 00000000..015832c8
--- /dev/null
+++ b/connector_ecommerce/unit/sale_order_onchange.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# connector-ecommerce for OpenERP
+# Copyright (C) 2013-TODAY Akretion .
+# @author Sébastien BEAU
+#
+# 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 .
+#
+##############################################################################
+
+from openerp.addons.connector.connector import ConnectorUnit
+
+
+class OnChangeManager(ConnectorUnit):
+
+ def merge_values(self, record, on_change_result, model=None):
+ record.update(self.get_new_values(record, on_change_result,
+ model=model))
+
+ def get_new_values(self, record, on_change_result, model=None):
+ vals = on_change_result.get('value', {})
+ new_values = {}
+ for fieldname, value in vals.iteritems():
+ if fieldname not in record:
+ if model:
+ column = self.env[model]._fields[fieldname]
+ if column.type == 'many2many':
+ value = [(6, 0, value)]
+ new_values[fieldname] = value
+ return new_values
+
+
+class SaleOrderOnChange(OnChangeManager):
+ _model_name = None
+
+ def _get_partner_id_onchange_param(self, order):
+ """ Prepare the arguments for calling the partner_id change
+ on sales order. You can overwrite this method in your own
+ module if they modify the onchange signature
+
+ :param order: a dictionary of the value of your sales order
+ :type: dict
+
+ :return: a tuple of args and kwargs for the onchange
+ :rtype: tuple
+ """
+ args = [
+ order.get('partner_id'),
+ ]
+ kwargs = {}
+ return args, kwargs
+
+ def _play_order_onchange(self, order):
+ """ Play the onchange of the sales order
+
+ :param order: a dictionary of the value of your sales order
+ :type: dict
+
+ :return: the value of the sales order updated with the onchange result
+ :rtype: dict
+ """
+ sale_model = self.env['sale.order']
+ onchange_specs = sale_model._onchange_spec()
+
+ # we need all fields in the dict even the empty ones
+ # otherwise 'onchange()' will not apply changes to them
+ all_values = order.copy()
+ for field in sale_model._fields:
+ if field not in all_values:
+ all_values[field] = False
+
+ # we work on a temporary record
+ order_record = sale_model.new(all_values)
+
+ new_values = {}
+
+ # Play partner_id onchange
+ args, kwargs = self._get_partner_id_onchange_param(order)
+ values = order_record.onchange_partner_id(*args, **kwargs)
+ new_values.update(self.get_new_values(order, values,
+ model='sale.order'))
+ all_values.update(new_values)
+
+ values = order_record.onchange(all_values,
+ 'payment_method_id',
+ onchange_specs)
+ new_values.update(self.get_new_values(order, values,
+ model='sale.order'))
+ all_values.update(new_values)
+
+ values = order_record.onchange(all_values,
+ 'workflow_process_id',
+ onchange_specs)
+ new_values.update(self.get_new_values(order, values,
+ model='sale.order'))
+ all_values.update(new_values)
+
+ res = {f: v for f, v in all_values.iteritems()
+ if f in order or f in new_values}
+ return res
+
+ def _get_product_id_onchange_param(self, line, previous_lines, order):
+ """ Prepare the arguments for calling the product_id change
+ on sales order line. You can overwrite this method in your own
+ module if they modify the onchange signature
+
+ :param line: the sales order line to process
+ :type: dict
+ :param previous_lines: list of dict of the previous lines processed
+ :type: list
+ :param order: data of the sales order
+ :type: dict
+
+ :return: a tuple of args and kwargs for the onchange
+ :rtype: tuple
+ """
+ args = [
+ order.get('pricelist_id'),
+ line.get('product_id'),
+ ]
+
+ # used in sale_markup: this is to ensure the unit price
+ # sent by the e-commerce connector is used for markup calculation
+ onchange_context = self.env.context.copy()
+ if line.get('price_unit'):
+ onchange_context.update({'unit_price': line.get('price_unit'),
+ 'force_unit_price': True})
+
+ uos_qty = float(line.get('product_uos_qty', 0))
+ if not uos_qty:
+ uos_qty = float(line.get('product_uom_qty', 0))
+
+ kwargs = {
+ 'qty': float(line.get('product_uom_qty', 0)),
+ 'uom': line.get('product_uom'),
+ 'qty_uos': uos_qty,
+ 'uos': line.get('product_uos'),
+ 'name': line.get('name'),
+ 'partner_id': order.get('partner_id'),
+ 'lang': False,
+ 'update_tax': True,
+ 'date_order': order.get('date_order'),
+ 'packaging': line.get('product_packaging'),
+ 'fiscal_position': order.get('fiscal_position'),
+ 'flag': False,
+ 'context': onchange_context,
+ }
+ return args, kwargs
+
+ def _play_line_onchange(self, line, previous_lines, order):
+ """ Play the onchange of the sales order line
+
+ :param line: the sales order line to process
+ :type: dict
+ :param previous_lines: list of dict of the previous line processed
+ :type: list
+ :param order: data of the sales order
+ :type: dict
+
+ :return: the value of the sales order updated with the onchange result
+ :rtype: dict
+ """
+ line_model = self.env['sale.order.line']
+ # Play product_id onchange
+ args, kwargs = self._get_product_id_onchange_param(line,
+ previous_lines,
+ order)
+ context = kwargs.pop('context', {})
+ values = line_model.with_context(context).product_id_change(*args,
+ **kwargs)
+ self.merge_values(line, values, model='sale.order.line')
+ return line
+
+ def play(self, order, order_lines):
+ """ Play the onchange of the sales order and it's lines
+
+ It expects to receive a recordset containing one sales order.
+ It could have been generated with
+ ``self.env['sale.order'].new(values)`` or
+ ``self.env['sale.order'].create(values)``.
+
+ :param order: data of the sales order
+ :type: recordset
+ :param order_lines: data of the sales order lines
+ :type: recordset
+
+ :return: the sales order updated by the onchanges
+ :rtype: recordset
+ """
+ # play onchange on sales order
+ order = self._play_order_onchange(order)
+
+ # play onchange on sales order line
+ processed_order_lines = []
+ line_lists = [order_lines]
+ if 'order_line' in order and order['order_line'] is not order_lines:
+ # we have both backend-dependent and oerp-native order
+ # lines.
+ # oerp-native lines can have been added to map
+ # shipping fees with an OpenERP Product
+ line_lists.append(order['order_line'])
+ for line_list in line_lists:
+ for idx, command_line in enumerate(line_list):
+ # line_list format:[(0, 0, {...}), (0, 0, {...})]
+ if command_line[0] in (0, 1): # create or update values
+ # keeps command number and ID (or 0)
+ old_line_data = command_line[2]
+ new_line_data = self._play_line_onchange(
+ old_line_data, processed_order_lines, order)
+ new_line = (command_line[0],
+ command_line[1],
+ new_line_data)
+ processed_order_lines.append(new_line)
+ # in place modification of the sales order line in the list
+ line_list[idx] = new_line
+ return order
diff --git a/connector_ecommerce/wizard/__init__.py b/connector_ecommerce/wizard/__init__.py
new file mode 100644
index 00000000..3935a40e
--- /dev/null
+++ b/connector_ecommerce/wizard/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import sale_ignore_cancel
diff --git a/__unported__/connector_ecommerce/wizard/sale_ignore_cancel.py b/connector_ecommerce/wizard/sale_ignore_cancel.py
similarity index 58%
rename from __unported__/connector_ecommerce/wizard/sale_ignore_cancel.py
rename to connector_ecommerce/wizard/sale_ignore_cancel.py
index aa57c9dd..e58de7db 100644
--- a/__unported__/connector_ecommerce/wizard/sale_ignore_cancel.py
+++ b/connector_ecommerce/wizard/sale_ignore_cancel.py
@@ -19,28 +19,20 @@
#
##############################################################################
-from openerp.osv import orm, fields
+from openerp import models, fields, api
-class sale_ignore_cancel(orm.TransientModel):
+class SaleIgnoreCancel(models.TransientModel):
_name = 'sale.ignore.cancel'
_description = 'Ignore Sales Order Cancel'
- _columns = {
- 'reason': fields.html('Reason', required=True),
- }
+ reason = fields.Html(required=True)
- def confirm_ignore_cancel(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
- if isinstance(ids, (list, tuple)):
- assert len(ids) == 1
- ids = ids[0]
- order_ids = context.get('active_ids')
- if order_ids is None:
- return
- form = self.browse(cr, uid, ids, context=context)
- self.pool.get('sale.order').ignore_cancellation(cr, uid, order_ids,
- form.reason,
- context=context)
+ @api.multi
+ def confirm_ignore_cancel(self):
+ self.ensure_one()
+ sale_ids = self.env.context.get('active_ids')
+ assert sale_ids
+ sales = self.env['sale.order'].browse(sale_ids)
+ sales.ignore_cancellation(self.reason)
return {'type': 'ir.actions.act_window_close'}
diff --git a/__unported__/connector_ecommerce/wizard/sale_ignore_cancel_view.xml b/connector_ecommerce/wizard/sale_ignore_cancel_view.xml
similarity index 100%
rename from __unported__/connector_ecommerce/wizard/sale_ignore_cancel_view.xml
rename to connector_ecommerce/wizard/sale_ignore_cancel_view.xml