- -
[ ]
diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py
index 0f3d0cb622f..6b1c9aaf7e8 100644
--- a/addons/mrp/wizard/change_production_qty.py
+++ b/addons/mrp/wizard/change_production_qty.py
@@ -78,7 +78,7 @@ class change_production_qty(osv.osv_memory):
bom_point = prod.bom_id
bom_id = prod.bom_id.id
if not bom_point:
- bom_id = bom_obj._bom_find(cr, uid, prod.product_id.id, prod.product_uom.id)
+ bom_id = bom_obj._bom_find(cr, uid, prod.product_uom.id, product_id=prod.product_id.id)
if not bom_id:
raise osv.except_osv(_('Error!'), _("Cannot find bill of material for this product."))
prod_obj.write(cr, uid, [prod.id], {'bom_id': bom_id})
@@ -89,7 +89,7 @@ class change_production_qty(osv.osv_memory):
factor = prod.product_qty * prod.product_uom.factor / bom_point.product_uom.factor
product_details, workcenter_details = \
- bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, [])
+ bom_obj._bom_explode(cr, uid, bom_point, prod.product_id, factor / bom_point.product_qty, [])
for r in product_details:
if r['product_id'] == move.product_id.id:
move_obj.write(cr, uid, [move.id], {'product_uom_qty': r['product_qty']})
diff --git a/addons/mrp_byproduct/test/mrp_byproduct.yml b/addons/mrp_byproduct/test/mrp_byproduct.yml
index cd6ce885256..ac1a114ef15 100644
--- a/addons/mrp_byproduct/test/mrp_byproduct.yml
+++ b/addons/mrp_byproduct/test/mrp_byproduct.yml
@@ -4,7 +4,7 @@
-
I add a sub product in Bill of material for product External Hard Disk.
-
- !record {model: mrp.bom, id: mrp.mrp_bom_24}:
+ !record {model: mrp.bom, id: mrp.mrp_bom_9}:
product_id: product.product_product_28
name: External Hard Disk + Subproduct
product_uom: product.product_uom_unit
@@ -20,7 +20,7 @@
product_id: product.product_product_28
product_qty: 2.0
product_uom: product.product_uom_unit
- bom_id: mrp.mrp_bom_24
+ bom_id: mrp.mrp_bom_9
location_src_id: stock.stock_location_stock
-
I compute the data of production order.
diff --git a/addons/mrp_repair/views/report_mrprepairorder.xml b/addons/mrp_repair/views/report_mrprepairorder.xml
index 0ea3f22167c..2897fee1fd9 100644
--- a/addons/mrp_repair/views/report_mrprepairorder.xml
+++ b/addons/mrp_repair/views/report_mrprepairorder.xml
@@ -22,7 +22,7 @@
VAT:
-
+
diff --git a/addons/pad/static/src/img/pad_link_companies.jpeg b/addons/pad/static/src/img/pad_link_companies.jpeg
new file mode 100644
index 00000000000..1bce3a56186
Binary files /dev/null and b/addons/pad/static/src/img/pad_link_companies.jpeg differ
diff --git a/addons/payment/models/res_config.py b/addons/payment/models/res_config.py
index 70668a296da..a74e595b52e 100644
--- a/addons/payment/models/res_config.py
+++ b/addons/payment/models/res_config.py
@@ -16,4 +16,7 @@ class AccountPaymentConfig(osv.TransientModel):
'module_payment_adyen': fields.boolean(
'Manage Payments Using Adyen',
help='-It installs the module payment_adyen.'),
+ 'module_payment_buckaroo': fields.boolean(
+ 'Manage Payments Using Buckaroo',
+ help='-It installs the module payment_buckaroo.'),
}
diff --git a/addons/payment/views/res_config_view.xml b/addons/payment/views/res_config_view.xml
index 101acb7c9ee..45c0a3d168e 100644
--- a/addons/payment/views/res_config_view.xml
+++ b/addons/payment/views/res_config_view.xml
@@ -20,6 +20,10 @@
+
+
+
+
diff --git a/addons/product_manufacturer/__init__.py b/addons/payment_buckaroo/__init__.py
similarity index 85%
rename from addons/product_manufacturer/__init__.py
rename to addons/payment_buckaroo/__init__.py
index 9b2446b4a44..1f4b1b74e57 100644
--- a/addons/product_manufacturer/__init__.py
+++ b/addons/payment_buckaroo/__init__.py
@@ -1,7 +1,8 @@
+# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
+# Copyright (C) 2014-Today OpenERP 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
@@ -18,7 +19,5 @@
#
##############################################################################
-import product_manufacturer
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
-
+import models
+import controllers
diff --git a/addons/payment_buckaroo/__openerp__.py b/addons/payment_buckaroo/__openerp__.py
new file mode 100644
index 00000000000..526f7c38612
--- /dev/null
+++ b/addons/payment_buckaroo/__openerp__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Buckaroo Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Buckaroo Implementation',
+ 'version': '1.0',
+ 'description': """Buckaroo Payment Acquirer""",
+ 'author': 'OpenERP SA',
+ 'depends': ['payment'],
+ 'data': [
+ 'views/buckaroo.xml',
+ 'views/payment_acquirer.xml',
+ 'data/buckaroo.xml',
+ ],
+ 'installable': True,
+}
diff --git a/addons/payment_buckaroo/controllers/__init__.py b/addons/payment_buckaroo/controllers/__init__.py
new file mode 100644
index 00000000000..bbd183e955b
--- /dev/null
+++ b/addons/payment_buckaroo/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import main
diff --git a/addons/payment_buckaroo/controllers/main.py b/addons/payment_buckaroo/controllers/main.py
new file mode 100644
index 00000000000..d3fe5b196fc
--- /dev/null
+++ b/addons/payment_buckaroo/controllers/main.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import logging
+import pprint
+import werkzeug
+
+from openerp import http, SUPERUSER_ID
+from openerp.http import request
+
+_logger = logging.getLogger(__name__)
+
+
+class BuckarooController(http.Controller):
+ _return_url = '/payment/buckaroo/return'
+ _cancel_url = '/payment/buckaroo/cancel'
+ _exception_url = '/payment/buckaroo/error'
+ _reject_url = '/payment/buckaroo/reject'
+
+ @http.route([
+ '/payment/buckaroo/return',
+ '/payment/buckaroo/cancel',
+ '/payment/buckaroo/error',
+ '/payment/buckaroo/reject',
+ ], type='http', auth='none')
+ def buckaroo_return(self, **post):
+ """ Buckaroo."""
+ _logger.info('Buckaroo: entering form_feedback with post data %s', pprint.pformat(post)) # debug
+ request.registry['payment.transaction'].form_feedback(request.cr, SUPERUSER_ID, post, 'buckaroo', context=request.context)
+ return_url = post.pop('return_url', '')
+ if not return_url:
+ data ='' + post.pop('ADD_RETURNDATA', '{}').replace("'", "\"")
+ custom = json.loads(data)
+ return_url = custom.pop('return_url', '/')
+ return werkzeug.utils.redirect(return_url)
diff --git a/addons/payment_buckaroo/data/buckaroo.xml b/addons/payment_buckaroo/data/buckaroo.xml
new file mode 100644
index 00000000000..9d100a59191
--- /dev/null
+++ b/addons/payment_buckaroo/data/buckaroo.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Buckaroo
+ buckaroo
+
+
+ test
+ You will be redirected to the Buckaroo website after cliking on the payment button.]]>
+ dummy
+ dummy
+
+
+
+
diff --git a/addons/payment_buckaroo/models/__init__.py b/addons/payment_buckaroo/models/__init__.py
new file mode 100644
index 00000000000..7e1780a0059
--- /dev/null
+++ b/addons/payment_buckaroo/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import buckaroo
diff --git a/addons/payment_buckaroo/models/buckaroo.py b/addons/payment_buckaroo/models/buckaroo.py
new file mode 100644
index 00000000000..5b576a24c3b
--- /dev/null
+++ b/addons/payment_buckaroo/models/buckaroo.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-'8' "-*-"
+from hashlib import sha1
+import logging
+import urlparse
+
+from openerp.addons.payment.models.payment_acquirer import ValidationError
+from openerp.addons.payment_buckaroo.controllers.main import BuckarooController
+from openerp.osv import osv, fields
+from openerp.tools.float_utils import float_compare
+
+_logger = logging.getLogger(__name__)
+
+
+class AcquirerBuckaroo(osv.Model):
+ _inherit = 'payment.acquirer'
+
+ def _get_buckaroo_urls(self, cr, uid, environment, context=None):
+ """ Buckaroo URLs
+ """
+ if environment == 'prod':
+ return {
+ 'buckaroo_form_url': 'https://checkout.buckaroo.nl/html/',
+ }
+ else:
+ return {
+ 'buckaroo_form_url': 'https://testcheckout.buckaroo.nl/html/',
+ }
+
+ def _get_providers(self, cr, uid, context=None):
+ providers = super(AcquirerBuckaroo, self)._get_providers(cr, uid, context=context)
+ providers.append(['buckaroo', 'Buckaroo'])
+ return providers
+
+ _columns = {
+ 'brq_websitekey': fields.char('WebsiteKey', required_if_provider='buckaroo'),
+ 'brq_secretkey': fields.char('SecretKey', required_if_provider='buckaroo'),
+ }
+
+ def _buckaroo_generate_digital_sign(self, acquirer, inout, values):
+ """ Generate the shasign for incoming or outgoing communications.
+
+ :param browse acquirer: the payment.acquirer browse record. It should
+ have a shakey in shaky out
+ :param string inout: 'in' (openerp contacting buckaroo) or 'out' (buckaroo
+ contacting openerp).
+ :param dict values: transaction values
+
+ :return string: shasign
+ """
+ assert inout in ('in', 'out')
+ assert acquirer.provider == 'buckaroo'
+
+ keys = "add_returndata Brq_amount Brq_culture Brq_currency Brq_invoicenumber Brq_return Brq_returncancel Brq_returnerror Brq_returnreject brq_test Brq_websitekey".split()
+
+ def get_value(key):
+ if values.get(key):
+ return values[key]
+ return ''
+
+ if inout == 'out':
+ if 'BRQ_SIGNATURE' in values:
+ del values['BRQ_SIGNATURE']
+ items = sorted((k.upper(), v) for k, v in values.items())
+ sign = ''.join('%s=%s' % (k, v) for k, v in items)
+ else:
+ sign = ''.join('%s=%s' % (k,get_value(k)) for k in keys)
+ #Add the pre-shared secret key at the end of the signature
+ sign = sign + acquirer.brq_secretkey
+ if isinstance(sign, str):
+ sign = urlparse.parse_qsl(sign)
+ shasign = sha1(sign).hexdigest()
+ return shasign
+
+
+ def buckaroo_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
+ base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
+ acquirer = self.browse(cr, uid, id, context=context)
+ buckaroo_tx_values = dict(tx_values)
+ buckaroo_tx_values.update({
+ 'Brq_websitekey': acquirer.brq_websitekey,
+ 'Brq_amount': tx_values['amount'],
+ 'Brq_currency': tx_values['currency'] and tx_values['currency'].name or '',
+ 'Brq_invoicenumber': tx_values['reference'],
+ 'brq_test' : True,
+ 'Brq_return': '%s' % urlparse.urljoin(base_url, BuckarooController._return_url),
+ 'Brq_returncancel': '%s' % urlparse.urljoin(base_url, BuckarooController._cancel_url),
+ 'Brq_returnerror': '%s' % urlparse.urljoin(base_url, BuckarooController._exception_url),
+ 'Brq_returnreject': '%s' % urlparse.urljoin(base_url, BuckarooController._reject_url),
+ 'Brq_culture': 'en-US',
+ })
+ if buckaroo_tx_values.get('return_url'):
+ buckaroo_tx_values['add_returndata'] = {'return_url': '%s' % buckaroo_tx_values.pop('return_url')}
+ else:
+ buckaroo_tx_values['add_returndata'] = ''
+ buckaroo_tx_values['Brq_signature'] = self._buckaroo_generate_digital_sign(acquirer, 'in', buckaroo_tx_values)
+ return partner_values, buckaroo_tx_values
+
+ def buckaroo_get_form_action_url(self, cr, uid, id, context=None):
+ acquirer = self.browse(cr, uid, id, context=context)
+ return self._get_buckaroo_urls(cr, uid, acquirer.environment, context=context)['buckaroo_form_url']
+
+class TxBuckaroo(osv.Model):
+ _inherit = 'payment.transaction'
+
+ # buckaroo status
+ _buckaroo_valid_tx_status = [190]
+ _buckaroo_pending_tx_status = [790, 791, 792, 793]
+ _buckaroo_cancel_tx_status = [890, 891]
+ _buckaroo_error_tx_status = [490, 491, 492]
+ _buckaroo_reject_tx_status = [690]
+
+ _columns = {
+ 'buckaroo_txnid': fields.char('Transaction ID'),
+ }
+
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ def _buckaroo_form_get_tx_from_data(self, cr, uid, data, context=None):
+ """ Given a data dict coming from buckaroo, verify it and find the related
+ transaction record. """
+ reference, pay_id, shasign = data.get('BRQ_INVOICENUMBER'), data.get('BRQ_PAYMENT'), data.get('BRQ_SIGNATURE')
+ if not reference or not pay_id or not shasign:
+ error_msg = 'Buckaroo: received data with missing reference (%s) or pay_id (%s) or shashign (%s)' % (reference, pay_id, shasign)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ tx_ids = self.search(cr, uid, [('reference', '=', reference)], context=context)
+ if not tx_ids or len(tx_ids) > 1:
+ error_msg = 'Buckaroo: received data for reference %s' % (reference)
+ if not tx_ids:
+ error_msg += '; no order found'
+ else:
+ error_msg += '; multiple order found'
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
+
+ #verify shasign
+ shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out' ,data)
+ if shasign_check.upper() != shasign.upper():
+ error_msg = 'Buckaroo: invalid shasign, received %s, computed %s, for data %s' % (shasign, shasign_check, data)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ return tx
+
+ def _buckaroo_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
+ invalid_parameters = []
+
+ if tx.acquirer_reference and data.get('BRQ_TRANSACTIONS') != tx.acquirer_reference:
+ invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), tx.acquirer_reference))
+ # check what is buyed
+ if float_compare(float(data.get('BRQ_AMOUNT', '0.0')), tx.amount, 2) != 0:
+ invalid_parameters.append(('Amount', data.get('BRQ_AMOUNT'), '%.2f' % tx.amount))
+ if data.get('BRQ_CURRENCY') != tx.currency_id.name:
+ invalid_parameters.append(('Currency', data.get('BRQ_CURRENCY'), tx.currency_id.name))
+
+ return invalid_parameters
+
+ def _buckaroo_form_validate(self, cr, uid, tx, data, context=None):
+ status_code = int(data.get('BRQ_STATUSCODE','0'))
+ if status_code in self._buckaroo_valid_tx_status:
+ tx.write({
+ 'state': 'done',
+ 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
+ })
+ return True
+ elif status_code in self._buckaroo_pending_tx_status:
+ tx.write({
+ 'state': 'pending',
+ 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
+ })
+ return True
+ elif status_code in self._buckaroo_cancel_tx_status:
+ tx.write({
+ 'state': 'cancel',
+ 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
+ })
+ return True
+ else:
+ error = 'Buckaroo: feedback error'
+ _logger.info(error)
+ tx.write({
+ 'state': 'error',
+ 'state_message': error,
+ 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'),
+ })
+ return False
diff --git a/addons/payment_buckaroo/static/description/icon.png b/addons/payment_buckaroo/static/description/icon.png
new file mode 100644
index 00000000000..663fcad1d6d
Binary files /dev/null and b/addons/payment_buckaroo/static/description/icon.png differ
diff --git a/addons/payment_buckaroo/static/src/img/buckaroo_icon.png b/addons/payment_buckaroo/static/src/img/buckaroo_icon.png
new file mode 100644
index 00000000000..819606db730
Binary files /dev/null and b/addons/payment_buckaroo/static/src/img/buckaroo_icon.png differ
diff --git a/addons/payment_buckaroo/static/src/img/logo.png b/addons/payment_buckaroo/static/src/img/logo.png
new file mode 100644
index 00000000000..663fcad1d6d
Binary files /dev/null and b/addons/payment_buckaroo/static/src/img/logo.png differ
diff --git a/addons/payment_buckaroo/tests/__init__.py b/addons/payment_buckaroo/tests/__init__.py
new file mode 100644
index 00000000000..d245ab8339d
--- /dev/null
+++ b/addons/payment_buckaroo/tests/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from openerp.addons.payment_buckaroo.tests import test_buckaroo
+
+checks = [
+ test_buckaroo,
+]
diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py
new file mode 100644
index 00000000000..b826f3b920e
--- /dev/null
+++ b/addons/payment_buckaroo/tests/test_buckaroo.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+
+from lxml import objectify
+import urlparse
+
+import openerp
+from openerp.addons.payment.models.payment_acquirer import ValidationError
+from openerp.addons.payment.tests.common import PaymentAcquirerCommon
+from openerp.addons.payment_buckaroo.controllers.main import BuckarooController
+from openerp.tools import mute_logger
+
+
+@openerp.tests.common.at_install(False)
+@openerp.tests.common.post_install(False)
+class BuckarooCommon(PaymentAcquirerCommon):
+
+ def setUp(self):
+ super(BuckarooCommon, self).setUp()
+ cr, uid = self.cr, self.uid
+ self.base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url')
+
+ # get the buckaroo account
+ model, self.buckaroo_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_buckaroo', 'payment_acquirer_buckaroo')
+
+
+@openerp.tests.common.at_install(False)
+@openerp.tests.common.post_install(False)
+class BuckarooForm(BuckarooCommon):
+
+ def test_10_Buckaroo_form_render(self):
+ cr, uid, context = self.cr, self.uid, {}
+ # be sure not to do stupid things
+ buckaroo = self.payment_acquirer.browse(self.cr, self.uid, self.buckaroo_id, None)
+ self.assertEqual(buckaroo.environment, 'test', 'test without test environment')
+
+ # ----------------------------------------
+ # Test: button direct rendering
+ # ----------------------------------------
+
+ form_values = {
+ 'add_returndata': None,
+ 'Brq_websitekey': buckaroo.brq_websitekey,
+ 'Brq_amount': '2240.0',
+ 'Brq_currency': 'EUR',
+ 'Brq_invoicenumber': 'SO004',
+ 'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474', # update me
+ 'brq_test': 'True',
+ 'Brq_return': '%s' % urlparse.urljoin(self.base_url, BuckarooController._return_url),
+ 'Brq_returncancel': '%s' % urlparse.urljoin(self.base_url, BuckarooController._cancel_url),
+ 'Brq_returnerror': '%s' % urlparse.urljoin(self.base_url, BuckarooController._exception_url),
+ 'Brq_returnreject': '%s' % urlparse.urljoin(self.base_url, BuckarooController._reject_url),
+ 'Brq_culture': 'en-US',
+ }
+
+ # render the button
+ res = self.payment_acquirer.render(
+ cr, uid, self.buckaroo_id,
+ 'SO004', 2240.0, self.currency_euro_id,
+ partner_id=None,
+ partner_values=self.buyer_values,
+ context=context)
+
+ # check form result
+ tree = objectify.fromstring(res)
+ self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
+ for form_input in tree.input:
+ if form_input.get('name') in ['submit']:
+ continue
+ self.assertEqual(
+ form_input.get('value'),
+ form_values[form_input.get('name')],
+ 'Buckaroo: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
+ )
+
+ # ----------------------------------------
+ # Test2: button using tx + validation
+ # ----------------------------------------
+
+ # create a new draft tx
+ tx_id = self.payment_transaction.create(
+ cr, uid, {
+ 'amount': 2240.0,
+ 'acquirer_id': self.buckaroo_id,
+ 'currency_id': self.currency_euro_id,
+ 'reference': 'SO004',
+ 'partner_id': self.buyer_id,
+ }, context=context
+ )
+
+ # render the button
+ res = self.payment_acquirer.render(
+ cr, uid, self.buckaroo_id,
+ 'should_be_erased', 2240.0, self.currency_euro,
+ tx_id=tx_id,
+ partner_id=None,
+ partner_values=self.buyer_values,
+ context=context)
+
+ # check form result
+ tree = objectify.fromstring(res)
+ self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
+ for form_input in tree.input:
+ if form_input.get('name') in ['submit']:
+ continue
+ self.assertEqual(
+ form_input.get('value'),
+ form_values[form_input.get('name')],
+ 'Buckaroo: wrong value for form input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
+ )
+
+ @mute_logger('openerp.addons.payment_buckaroo.models.buckaroo', 'ValidationError')
+ def test_20_buckaroo_form_management(self):
+ cr, uid, context = self.cr, self.uid, {}
+ # be sure not to do stupid thing
+ buckaroo = self.payment_acquirer.browse(self.cr, self.uid, self.buckaroo_id, None)
+ self.assertEqual(buckaroo.environment, 'test', 'test without test environment')
+
+ # typical data posted by buckaroo after client has successfully paid
+ buckaroo_post_data = {
+ 'BRQ_RETURNDATA': u'',
+ 'BRQ_AMOUNT': u'2240.00',
+ 'BRQ_CURRENCY': u'EUR',
+ 'BRQ_CUSTOMER_NAME': u'Jan de Tester',
+ 'BRQ_INVOICENUMBER': u'SO004',
+ 'BRQ_PAYMENT': u'573311D081B04069BD6336001611DBD4',
+ 'BRQ_PAYMENT_METHOD': u'paypal',
+ 'BRQ_SERVICE_PAYPAL_PAYERCOUNTRY': u'NL',
+ 'BRQ_SERVICE_PAYPAL_PAYEREMAIL': u'fhe@openerp.com',
+ 'BRQ_SERVICE_PAYPAL_PAYERFIRSTNAME': u'Jan',
+ 'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester',
+ 'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de',
+ 'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified',
+ 'BRQ_SIGNATURE': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91',
+ 'BRQ_STATUSCODE': u'190',
+ 'BRQ_STATUSCODE_DETAIL': u'S001',
+ 'BRQ_STATUSMESSAGE': u'Transaction successfully processed',
+ 'BRQ_TEST': u'true',
+ 'BRQ_TIMESTAMP': u'2014-05-08 12:41:21',
+ 'BRQ_TRANSACTIONS': u'D6106678E1D54EEB8093F5B3AC42EA7B',
+ 'BRQ_WEBSITEKEY': u'5xTGyGyPyl',
+ }
+
+ # should raise error about unknown tx
+ with self.assertRaises(ValidationError):
+ self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
+
+ tx_id = self.payment_transaction.create(
+ cr, uid, {
+ 'amount': 2240.0,
+ 'acquirer_id': self.buckaroo_id,
+ 'currency_id': self.currency_euro_id,
+ 'reference': 'SO004',
+ 'partner_name': 'Norbert Buyer',
+ 'partner_country_id': self.country_france_id,
+ }, context=context
+ )
+ # validate it
+ self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
+ # check state
+ tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
+ self.assertEqual(tx.state, 'done', 'Buckaroo: validation did not put tx into done state')
+ self.assertEqual(tx.buckaroo_txnid, buckaroo_post_data.get('BRQ_TRANSACTIONS'), 'Buckaroo: validation did not update tx payid')
+
+ # reset tx
+ tx.write({'state': 'draft', 'date_validate': False, 'buckaroo_txnid': False})
+
+ # now buckaroo post is ok: try to modify the SHASIGN
+ buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a'
+ with self.assertRaises(ValidationError):
+ self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
+
+ # simulate an error
+ buckaroo_post_data['BRQ_STATUSCODE'] = 2
+ buckaroo_post_data['BRQ_SIGNATURE'] = '4164b52adb1e6a2221d3d8a39d8c3e18a9ecb90b'
+ self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context)
+ # check state
+ tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
+ self.assertEqual(tx.state, 'error', 'Buckaroo: erroneous validation did not put tx into error state')
diff --git a/addons/payment_buckaroo/views/buckaroo.xml b/addons/payment_buckaroo/views/buckaroo.xml
new file mode 100644
index 00000000000..567a2691b00
--- /dev/null
+++ b/addons/payment_buckaroo/views/buckaroo.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_buckaroo/views/payment_acquirer.xml b/addons/payment_buckaroo/views/payment_acquirer.xml
new file mode 100644
index 00000000000..4eb294e49c3
--- /dev/null
+++ b/addons/payment_buckaroo/views/payment_acquirer.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ acquirer.form.buckaroo
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+
+
+ acquirer.transaction.form.buckaroo
+ payment.transaction
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_paypal/models/paypal.py b/addons/payment_paypal/models/paypal.py
index d71e3d0620b..5dcbd975a4a 100644
--- a/addons/payment_paypal/models/paypal.py
+++ b/addons/payment_paypal/models/paypal.py
@@ -95,9 +95,12 @@ class AcquirerPaypal(osv.Model):
return 0.0
country = self.pool['res.country'].browse(cr, uid, country_id, context=context)
if country and acquirer.company_id.country_id.id == country.id:
- fees = amount * (1 + acquirer.fees_dom_var / 100.0) + acquirer.fees_dom_fixed - amount
+ percentage = acquirer.fees_dom_var
+ fixed = acquirer.fees_dom_fixed
else:
- fees = amount * (1 + acquirer.fees_int_var / 100.0) + acquirer.fees_int_fixed - amount
+ percentage = acquirer.fees_int_var
+ fixed = acquirer.fees_int_fixed
+ fees = (percentage / 100.0 * amount + fixed ) / (1 - percentage / 100.0)
return fees
def paypal_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
diff --git a/addons/payment_paypal/tests/test_paypal.py b/addons/payment_paypal/tests/test_paypal.py
index ccac9b6fdef..a66b90cf0fb 100644
--- a/addons/payment_paypal/tests/test_paypal.py
+++ b/addons/payment_paypal/tests/test_paypal.py
@@ -148,7 +148,7 @@ class PaypalForm(PaypalCommon):
for form_input in tree.input:
if form_input.get('name') in ['handling']:
handling_found = True
- self.assertEqual(form_input.get('value'), '1.56', 'paypal: wrong computed fees')
+ self.assertEqual(form_input.get('value'), '1.57', 'paypal: wrong computed fees')
self.assertTrue(handling_found, 'paypal: fees_active did not add handling input in rendered form')
@mute_logger('openerp.addons.payment_paypal.models.paypal', 'ValidationError')
diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py
index b7959832ead..c1172c0cc6d 100644
--- a/addons/point_of_sale/point_of_sale.py
+++ b/addons/point_of_sale/point_of_sale.py
@@ -1316,30 +1316,80 @@ class ean_wizard(osv.osv_memory):
self.pool[m].write(cr,uid,[m_id],{'ean13':ean13})
return { 'type' : 'ir.actions.act_window_close' }
-class product_product(osv.osv):
- _inherit = 'product.product'
+class pos_category(osv.osv):
+ _name = "pos.category"
+ _description = "Public Category"
+ _order = "sequence, name"
+ _constraints = [
+ (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
+ ]
- #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None):
- # result = {}
- # for obj in self.browse(cr, uid, ids, context=context):
- # if not obj.product_image:
- # result[obj.id] = False
- # continue
+ def name_get(self, cr, uid, ids, context=None):
+ if not len(ids):
+ return []
+ reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
+ res = []
+ for record in reads:
+ name = record['name']
+ if record['parent_id']:
+ name = record['parent_id'][1]+' / '+name
+ res.append((record['id'], name))
+ return res
- # image_stream = io.BytesIO(obj.product_image.decode('base64'))
- # img = Image.open(image_stream)
- # img.thumbnail((120, 100), Image.ANTIALIAS)
- # img_stream = StringIO.StringIO()
- # img.save(img_stream, "JPEG")
- # result[obj.id] = img_stream.getvalue().encode('base64')
- # return result
+ def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
+ res = self.name_get(cr, uid, ids, context=context)
+ return dict(res)
+
+ def _get_image(self, cr, uid, ids, name, args, context=None):
+ result = dict.fromkeys(ids, False)
+ for obj in self.browse(cr, uid, ids, context=context):
+ result[obj.id] = tools.image_get_resized_images(obj.image)
+ return result
+
+ def _set_image(self, cr, uid, id, name, value, args, context=None):
+ return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
+
+ _columns = {
+ 'name': fields.char('Name', required=True, translate=True),
+ 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
+ 'parent_id': fields.many2one('pos.category','Parent Category', select=True),
+ 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'),
+ 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
+
+ # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
+ # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
+ # In this case, the default image is set by the js code.
+ # NOTE2: image: all image fields are base64 encoded and PIL-supported
+ 'image': fields.binary("Image",
+ help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
+ 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
+ string="Medium-sized image", type="binary", multi="_get_image",
+ store={
+ 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+ },
+ help="Medium-sized image of the category. It is automatically "\
+ "resized as a 128x128px image, with aspect ratio preserved. "\
+ "Use this field in form views or some kanban views."),
+ 'image_small': fields.function(_get_image, fnct_inv=_set_image,
+ string="Smal-sized image", type="binary", multi="_get_image",
+ store={
+ 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
+ },
+ help="Small-sized image of the category. It is automatically "\
+ "resized as a 64x64px image, with aspect ratio preserved. "\
+ "Use this field anywhere a small image is required."),
+ }
+
+class product_template(osv.osv):
+ _inherit = 'product.template'
_columns = {
'income_pdt': fields.boolean('Point of Sale Cash In', help="Check if, this is a product you can use to put cash into a statement for the point of sale backend."),
'expense_pdt': fields.boolean('Point of Sale Cash Out', help="Check if, this is a product you can use to take cash from a statement for the point of sale backend, example: money lost, transfer to bank, etc."),
'available_in_pos': fields.boolean('Available in the Point of Sale', help='Check if you want this product to appear in the Point of Sale'),
'to_weight' : fields.boolean('To Weigh', help="Check if the product should be weighted (mainly used with self check-out interface)."),
+ 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', help="Those categories are used to group similar products for point of sale."),
}
_defaults = {
diff --git a/addons/point_of_sale/point_of_sale_demo.xml b/addons/point_of_sale/point_of_sale_demo.xml
index a0a68ece880..ea6d62449f8 100644
--- a/addons/point_of_sale/point_of_sale_demo.xml
+++ b/addons/point_of_sale/point_of_sale_demo.xml
@@ -39,145 +39,146 @@
-
+
-
+
+ Computers
-
+
Beverages
-
+
Water
-
+
Plain Water
-
+
Sparkling Water
-
+
Soda
-
+
Coke
-
+
Orange
-
+
Beers
-
+
Pils
-
+
Fruity Beers
-
+
Special Beers
-
+
Food
-
+
Pizza
-
+
Ice Cream
-
+
Chips
-
+
Fresh Fruits
-
+
Oranges
-
+
Apples
-
+
Other Citrus
-
+
Pears
-
+
Berries
-
+
Grapes
-
+
Fresh vegetables
-
+
Potatoes
-
+
Root vegetables
-
+
Tomatos
-
+
Onions / Garlic / Shallots
-
+
Other fresh vegetables
@@ -191,7 +192,7 @@
Boni Oranges
True
2100002000003
-
+
@@ -200,7 +201,7 @@
True
2.83
Orange Butterfly
-
+
True
@@ -213,7 +214,7 @@
Lemon
2301000000006
True
-
+
@@ -223,7 +224,7 @@
3.19
Stringers
True
-
+
@@ -233,7 +234,7 @@
1.98
Red grapefruit
True
-
+
@@ -244,7 +245,7 @@
2.09
Granny Smith apples
True
-
+
@@ -254,7 +255,7 @@
1.10
Jonagold apples
True
-
+
@@ -264,7 +265,7 @@
1.69
Golden Apples Perlim
True
-
+
@@ -275,7 +276,7 @@
1.70
Conference pears
True
-
+
@@ -286,7 +287,7 @@
5.70
Peach
True
-
+
@@ -297,7 +298,7 @@
Peaches
True
2300001000008
-
+
@@ -308,7 +309,7 @@
4.80
Black Grapes
True
-
+
@@ -319,7 +320,7 @@
1.39
Potatoes
True
-
+
@@ -330,7 +331,7 @@
2.20
Extra Flandria chicory
True
-
+
@@ -340,7 +341,7 @@
0.90
Carrots
True
-
+
@@ -350,7 +351,7 @@
2.10
Fennel
True
-
+
@@ -361,7 +362,7 @@
1.90
In Cluster Tomatoes
True
-
+
@@ -373,7 +374,7 @@
Onions
True
2100001000004
-
+
@@ -384,7 +385,7 @@
3.10
Red Pepper
True
-
+
@@ -394,7 +395,7 @@
3.00
Green Peppers
True
-
+
@@ -404,7 +405,7 @@
2.70
Yellow Peppers
True
-
+
@@ -414,7 +415,7 @@
2.29
Leeks
True
-
+
@@ -424,7 +425,7 @@
1.20
Zucchini
True
-
+
@@ -435,21 +436,21 @@
1.49
Coca-Cola Regular 1L
5449000054227
-
+
True
2.38
Coca-Cola Regular 2L
-
+
True
0.97
Coca-Cola Regular 50cl
-
+
@@ -457,7 +458,7 @@
0.51
Coca-Cola Regular 33cl
5449000000996
-
+
@@ -465,35 +466,35 @@
True
1.49
Coca-Cola Light 1L
-
+
True
2.38
Coca-Cola Light 2L
-
+
True
0.97
Coca-Cola Light 50cl
-
+
True
0.51
Coca-Cola Light 33cl
-
+
True
0.53
Coca-Cola Light 33cl Decaf
-
+
@@ -501,35 +502,35 @@
True
1.49
Coca-Cola Zero 1L
-
+
True
2.38
Coca-Cola Zero 2L
-
+
True
0.97
Coca-Cola Zero 50cl
-
+
True
0.51
Coca-Cola Zero 33cl
-
+
True
0.67
Coca-Cola Zero Decaf 33cl
-
+
@@ -537,21 +538,21 @@
True
2.83
Coca-Cola Light Lemon 2L
-
+
True
1.16
Coca-Cola Light Lemon 50cl
-
+
True
0.53
Coca-Cola Light Lemon 33cl
-
+
@@ -559,42 +560,42 @@
True
1.70
Pepsi 2L
-
+
True
0.43
Pepsi 33cl
-
+
True
1.71
Pepsi Max 2L
-
+
True
0.40
Pepsi Max 33cl
-
+
True
0.61
Pepsi Max 50cl
-
+
True
0.40
Pepsi Max Cool Lemon 33cl
-
+
@@ -602,63 +603,63 @@
True
0.75
Spa Fruit and Orange 50cl
-
+
True
0.72
Orangina 33cl
-
+
True
2.42
Orangina 1.5L
-
+
True
0.98
Fanta Orange 50cl
-
+
True
2.28
Fanta Orange 2L
-
+
True
0.51
Fanta Orange 33cl
-
+
True
0.84
Fanta Orange 25cl
-
+
True
2.08
Fanta Orange Zero 1.5L
-
+
True
0.53
Fanta Zero Orange 33cl
-
+
@@ -666,56 +667,56 @@
True
0.52
Evian 50cl
-
+
True
0.70
Evian 1L
-
+
True
1.26
2L Evian
-
+
True
0.40
Spa Reine 33cl
-
+
True
0.46
Spa Reine 50cl
-
+
True
0.65
Spa Reine 1L
-
+
True
1.30
Spa Reine 2L
-
+
True
0.34
Chaudfontaine 33cl
-
+
@@ -723,14 +724,14 @@
0.44
5449000111715
Chaudfontaine 50cl
-
+
True
0.86
Chaudfontaine 1.5l
-
+
@@ -738,63 +739,63 @@
True
0.38
Spa Barisart 33cl
-
+
True
0.49
Spa Barisart 50cl
-
+
True
1.00
Spa Barisart 1.5l
-
+
True
0.41
Chaudfontaine Petillante 33cl
-
+
True
0.57
Chaudfontaine Petillante 50cl
-
+
True
0.98
Chaudfontaine Petillante 1.5l
-
+
True
0.71
50cl Perrier
-
+
True
0.96
Perrier 1L
-
+
True
0.98
San Pellegrino 1L
-
+
@@ -802,35 +803,35 @@
True
1.19
Stella Artois 50cl
-
+
True
0.82
Stella Artois 33cl
-
+
True
0.95
Maes 50cl
-
+
True
0.77
Maes 33cl
-
+
True
0.97
Jupiler 50cl
-
+
@@ -838,7 +839,7 @@
0.77
5410228142027
Jupiler 33cl
-
+
@@ -846,49 +847,49 @@
True
2.53
Boon Framboise 37.5cl
-
+
True
1.54
Timmermans Geuze 37.5cl
-
+
True
1.7
Timmermans Kriek 37.5cl
-
+
True
1.56
Timmermans Faro 37.5cl
-
+
True
1.94
Lindemans sinful 37.5cl
-
+
True
1.51
Lindemans Kriek 37.5cl
-
+
True
1.04
Grisette Cherry 25cl
-
+
@@ -896,7 +897,7 @@
0.83
Belle-Vue Kriek 25cl
5410228193449
-
+
@@ -905,7 +906,7 @@
1.00
Leffe Brune 33cl
5410228142164
-
+
@@ -913,49 +914,49 @@
1.00
Leffe Blonde 33cl
5410228142218
-
+
True
1.16
Leffe Brune "9" 33cl
-
+
True
1.59
Orval 33cl
-
+
True
1.75
Rochefort "8" 33cl
-
+
True
1.46
Chimay Bleu 33cl
-
+
True
3.57
Chimay Bleu 75cl
-
+
True
1.02
Chimay Red 33cl
-
+
@@ -963,84 +964,84 @@
True
2.86
Dr. Oetker Ristorante Mozzarella
-
+
True
2.86
Dr. Oetker Ristorante Bolognese
-
+
True
2.86
Dr. Oetker Ristorante Funghi
-
+
True
2.86
Dr. Oetker Ristorante Hawaii
-
+
True
2.86
Dr. Oetker Ristorante Pollo
-
+
True
2.86
Dr. Oetker Ristorante Prosciutto
-
+
True
2.86
Dr. Oetker Ristorante Quattro Formaggi
-
+
True
2.86
Dr. Oetker Ristorante Speciale
-
+
True
2.86
Dr. Oetker Ristorante Spinaci
-
+
True
2.86
Dr. Oetker Ristorante Tonno
-
+
True
2.86
Dr. Oetker Ristorante Vegetable
-
+
True
2.67
Dr. Oetker La Margherita
-
+
@@ -1048,42 +1049,42 @@
True
0.33
Croky Paprika 45g
-
+
True
0.39
Croky Natural 45g
-
+
True
1.78
Croky Bolognese 250g
-
+
True
1.48
250g Lays Pickels
-
+
True
1.48
Lays Ketchup 250g
-
+
True
1.58
Lays Salt and Pepper Oven Baked 150g
-
+
@@ -1091,35 +1092,35 @@
True
1.54
Oven Baked Lays Paprika 150g
-
+
True
1.55
Lays Paprika XXL 300g
-
+
True
1.48
Lays Light Paprika 170g
-
+
True
1.48
Lays Light Paprika 170g
-
+
True
0.39
Lays Paprika 45g
-
+
@@ -1127,35 +1128,35 @@
True
1.54
Oven Baked Lays Natural 150g
-
+
True
1.55
Lays Natural XXL 300g
-
+
True
1.48
Lays Natural Light 170g
-
+
True
1.48
Lays Natural Light 170g
-
+
True
0.39
Lays Natural 45g
-
+
@@ -1163,35 +1164,35 @@
True
7.60
IJsboerke Chocolat 2.5L
-
+
True
7.60
IJsboerke Mocha 2.5L
-
+
True
7.40
IJsboerke Vanilla 2.5L
-
+
True
8.40
IJsboerke Stracciatella 2.5L
-
+
True
7.60
IJsboerke 2.5L White Lady
-
+
diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml
index dc8a5850e65..6d19d8b4d6d 100644
--- a/addons/point_of_sale/point_of_sale_view.xml
+++ b/addons/point_of_sale/point_of_sale_view.xml
@@ -477,13 +477,68 @@
-
- product.normal.form.inherit
- product.product
-
+
+
+ pos.category.form
+ pos.category
-
+
+
+
+
+ pos.category.tree
+ pos.category
+
+
+
+
+
+
+
+
+
+ Pos Product Categories
+ ir.actions.act_window
+ pos.category
+ form
+ tree,form
+
+
+
+ Click to define a new category.
+
+ Categories are used to browse your products through the
+ touchscreen interface.
+
+ If you put a photo on the category, the layout of the
+ touchscreen interface will automatically. We suggest not to put
+ a photo on categories for small (1024x768) screens.
+
+
+
+
+
+
+
+
+ product.template.form.inherit
+ product.template
+
+
+
+
@@ -491,13 +546,10 @@
-
+
-
-
-
= date)):
@@ -207,8 +210,13 @@ class product_pricelist(osv.osv):
categ = categ.parent_id
categ_ids = categ_ids.keys()
- prod_ids = [x.id for x in products]
- prod_tmpl_ids = [x.product_tmpl_id.id for x in products]
+ is_product_template = products[0]._name == "product.template"
+ if is_product_template:
+ prod_tmpl_ids = [tmpl.id for tmpl in products]
+ prod_ids = [product.id for product in tmpl.product_variant_ids for tmpl in products]
+ else:
+ prod_ids = [product.id for product in products]
+ prod_tmpl_ids = [product.product_tmpl_id.id for product in products]
# Load all rules
cr.execute(
@@ -234,10 +242,17 @@ class product_pricelist(osv.osv):
for rule in items:
if rule.min_quantity and qtyrule.product_tmpl_id.id:
- continue
- if rule.product_id and product.id<>rule.product_id.id:
- continue
+ if is_product_template:
+ if rule.product_tmpl_id and product.id<>rule.product_tmpl_id.id:
+ continue
+ if rule.product_id:
+ continue
+ else:
+ if rule.product_tmpl_id and product.product_tmpl_id.id<>rule.product_tmpl_id.id:
+ continue
+ if rule.product_id and product.id<>rule.product_id.id:
+ continue
+
if rule.categ_id:
cat = product.categ_id
while cat:
diff --git a/addons/product/product.py b/addons/product/product.py
index 28045886464..2369db49d43 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -29,6 +29,7 @@ from openerp import tools
from openerp.osv import osv, fields
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
+import psycopg2
import openerp.addons.decimal_precision as dp
@@ -273,7 +274,7 @@ class product_category(osv.osv):
_defaults = {
- 'type' : lambda *a : 'normal',
+ 'type' : 'normal',
}
_parent_name = "parent_id"
@@ -316,69 +317,78 @@ class produce_price_history(osv.osv):
}
-class product_public_category(osv.osv):
- _name = "product.public.category"
- _description = "Public Category"
- _order = "sequence, name"
+#----------------------------------------------------------
+# Product Attributes
+#----------------------------------------------------------
+class product_attribute(osv.osv):
+ _name = "product.attribute"
+ _description = "Product Attribute"
+ _columns = {
+ 'name': fields.char('Name', translate=True, required=True),
+ 'value_ids': fields.one2many('product.attribute.value', 'attribute_id', 'Values'),
+ }
- _constraints = [
- (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id'])
- ]
+class product_attribute_value(osv.osv):
+ _name = "product.attribute.value"
+ _order = 'sequence'
+ def _get_price_extra(self, cr, uid, ids, name, args, context=None):
+ result = dict.fromkeys(ids, 0)
+ if not context.get('active_id'):
+ return result
- def name_get(self, cr, uid, ids, context=None):
- if not len(ids):
- return []
- reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
- res = []
- for record in reads:
- name = record['name']
- if record['parent_id']:
- name = record['parent_id'][1]+' / '+name
- res.append((record['id'], name))
- return res
-
- def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
- res = self.name_get(cr, uid, ids, context=context)
- return dict(res)
-
- def _get_image(self, cr, uid, ids, name, args, context=None):
- result = dict.fromkeys(ids, False)
for obj in self.browse(cr, uid, ids, context=context):
- result[obj.id] = tools.image_get_resized_images(obj.image)
+ for price_id in obj.price_ids:
+ if price_id.product_tmpl_id.id == context.get('active_id'):
+ result[obj.id] = price_id.price_extra
+ break
return result
-
- def _set_image(self, cr, uid, id, name, value, args, context=None):
- return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
+
+ def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
+ if 'active_id' not in context:
+ return None
+ p_obj = self.pool['product.attribute.price']
+ p_ids = p_obj.search(cr, uid, [('value_id', '=', id), ('product_tmpl_id', '=', context['active_id'])], context=context)
+ if p_ids:
+ p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
+ else:
+ p_obj.create(cr, uid, p_ids, {
+ 'product_tmpl_id': context['active_id'],
+ 'value_id': id,
+ 'price_extra': value,
+ }, context=context)
_columns = {
- 'name': fields.char('Name', required=True, translate=True),
- 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
- 'parent_id': fields.many2one('product.public.category','Parent Category', select=True),
- 'child_id': fields.one2many('product.public.category', 'parent_id', string='Children Categories'),
- 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."),
-
- # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail
- # for at least one category, then we display a default image on the other, so that the buttons have consistent styling.
- # In this case, the default image is set by the js code.
- # NOTE2: image: all image fields are base64 encoded and PIL-supported
- 'image': fields.binary("Image",
- help="This field holds the image used as image for the cateogry, limited to 1024x1024px."),
- 'image_medium': fields.function(_get_image, fnct_inv=_set_image,
- string="Medium-sized image", type="binary", multi="_get_image",
- store={
- 'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
- },
- help="Medium-sized image of the category. It is automatically "\
- "resized as a 128x128px image, with aspect ratio preserved. "\
- "Use this field in form views or some kanban views."),
- 'image_small': fields.function(_get_image, fnct_inv=_set_image,
- string="Smal-sized image", type="binary", multi="_get_image",
- store={
- 'product.public.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
- },
- help="Small-sized image of the category. It is automatically "\
- "resized as a 64x64px image, with aspect ratio preserved. "\
- "Use this field anywhere a small image is required."),
+ 'sequence': fields.integer('Sequence', help="Determine the display order"),
+ 'name': fields.char('Value', translate=True, required=True),
+ 'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True),
+ 'product_ids': fields.many2many('product.product', id1='att_id', id2='prod_id', string='Variants', readonly=True),
+ 'price_extra': fields.function(_get_price_extra, type='float', string='Attribute Price Extra',
+ fnct_inv=_set_price_extra,
+ digits_compute=dp.get_precision('Product Price'),
+ help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
+ 'price_ids': fields.one2many('product.attribute.price', 'value_id', string='Attribute Prices', readonly=True),
+ }
+ _sql_constraints = [
+ ('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
+ ]
+ _defaults = {
+ 'price_extra': 0.0,
+ }
+
+class product_attribute_price(osv.osv):
+ _name = "product.attribute.price"
+ _columns = {
+ 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
+ 'value_id': fields.many2one('product.attribute.value', 'Product Attribute Value', required=True),
+ 'price_extra': fields.float('Price Extra', digits_compute=dp.get_precision('Product Price')),
+ }
+
+class product_attribute_line(osv.osv):
+ _name = "product.attribute.line"
+ _columns = {
+ 'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True),
+ 'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True),
+ 'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value'),
}
@@ -399,6 +409,42 @@ class product_template(osv.osv):
def _set_image(self, cr, uid, id, name, value, args, context=None):
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
+ def _is_product_variant(self, cr, uid, ids, name, arg, context=None):
+ return self.is_product_variant(cr, uid, ids, name, arg, context=context)
+
+ def is_product_variant(self, cr, uid, ids, name, arg, context=None):
+ prod = self.pool.get('product.product')
+ res = dict.fromkeys(ids, False)
+ ctx = dict(context, active_test=True)
+ for product in self.browse(cr, uid, ids, context=context):
+ res[product.id] = prod.search(cr, uid, [('product_tmpl_id','=',product.id)], context=ctx, count=True) == 1
+ return res
+
+
+ def _product_template_price(self, cr, uid, ids, name, arg, context=None):
+ plobj = self.pool.get('product.pricelist')
+ res = {}
+ quantity = context.get('quantity') or 1.0
+ pricelist = context.get('pricelist', False)
+ partner = context.get('partner', False)
+ if pricelist:
+ # Support context pricelists specified as display_name or ID for compatibility
+ if isinstance(pricelist, basestring):
+ pricelist_ids = plobj.name_search(
+ cr, uid, pricelist, operator='=', context=context, limit=1)
+ pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
+
+ if isinstance(pricelist, (int, long)):
+ products = self.browse(cr, uid, ids, context=context)
+ qtys = map(lambda x: (x, quantity, partner), products)
+ pl = plobj.browse(cr, uid, pricelist, context=context)
+ price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
+ for id in ids:
+ res[id] = price.get(id, 0.0)
+ for id in ids:
+ res.setdefault(id, 0.0)
+ return res
+
def get_history_price(self, cr, uid, product_tmpl, company_id, date=None, context=None):
if context is None:
context = {}
@@ -421,6 +467,12 @@ class product_template(osv.osv):
'company_id': company_id,
}, context=context)
+ def _get_product_variant_count(self, cr, uid, ids, name, arg, context=None):
+ res = {}
+ for product in self.browse(cr, uid, ids):
+ res[product.id] = len(product.product_variant_ids)
+ return res
+
_columns = {
'name': fields.char('Name', required=True, translate=True, select=True),
'product_manager': fields.many2one('res.users','Product Manager'),
@@ -434,9 +486,10 @@ class product_template(osv.osv):
"This description will be copied to every Sale Order, Delivery Order and Customer Invoice/Refund"),
'type': fields.selection([('consu', 'Consumable'),('service','Service')], 'Product Type', required=True, help="Consumable are product where you don't manage stock, a service is a non-material product provided by a company or an individual."),
'rental': fields.boolean('Can be Rent'),
- 'categ_id': fields.many2one('product.category','Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
- 'public_categ_id': fields.many2one('product.public.category','Public Category', help="Those categories are used to group similar products for public sales (eg.: point of sale, e-commerce)."),
+ 'categ_id': fields.many2one('product.category','Internal Category', required=True, change_default=True, domain="[('type','=','normal')]" ,help="Select category for the current product"),
+ 'price': fields.function(_product_template_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
'list_price': fields.float('Sale Price', digits_compute=dp.get_precision('Product Price'), help="Base price to compute the customer price. Sometimes called the catalog price."),
+ 'lst_price' : fields.related('list_price', type="float", string='Public Price', digits_compute=dp.get_precision('Product Price')),
'standard_price': fields.property(type = 'float', digits_compute=dp.get_precision('Product Price'),
help="Cost price of the product template used for standard stock valuation in accounting and used as a base price on purchase orders.",
groups="base.group_user", string="Cost Price"),
@@ -445,6 +498,7 @@ class product_template(osv.osv):
'weight_net': fields.float('Net Weight', digits_compute=dp.get_precision('Stock Weight'), help="The net weight in Kg."),
'warranty': fields.float('Warranty'),
'sale_ok': fields.boolean('Can be Sold', help="Specify if the product can be selected in a sales order line."),
+ 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
'state': fields.selection([('',''),
('draft', 'In Development'),
('sellable','Normal'),
@@ -458,13 +512,12 @@ class product_template(osv.osv):
help='Coefficient to convert default Unit of Measure to Unit of Sale\n'
' uos = uom * coeff'),
'mes_type': fields.selection((('fixed', 'Fixed'), ('variable', 'Variable')), 'Measure Type'),
- 'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
'company_id': fields.many2one('res.company', 'Company', select=1),
# image: all image fields are base64 encoded and PIL-supported
'image': fields.binary("Image",
help="This field holds the image used as image for the product, limited to 1024x1024px."),
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
- string="Medium-sized image", type="binary", multi="_get_image",
+ string="Medium-sized image", type="binary", multi="_get_image",
store={
'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},
@@ -479,13 +532,64 @@ class product_template(osv.osv):
help="Small-sized image of the product. It is automatically "\
"resized as a 64x64px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required."),
- 'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Product Variants', required=True),
+
+ 'packaging' : fields.one2many('product.packaging', 'product_id', 'Logistical Units',
+ help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
+
+ 'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
+ 'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
+ help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
+ 'seller_qty': fields.related('seller_ids','qty', type='float', string='Supplier Quantity',
+ help="This is minimum quantity to purchase from Main Supplier."),
+ 'seller_id': fields.related('seller_ids','name', type='many2one', relation='res.partner', string='Main Supplier',
+ help="Main Supplier who has highest priority in Supplier List."),
+
+ 'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
+ 'color': fields.integer('Color Index'),
+ 'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Only one product variant'),
+
+ 'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
+ 'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
+ 'product_variant_count': fields.function( _get_product_variant_count, type='integer', string='# of Product Variants'),
+
+ # related to display product product information if is_product_variant
+ 'ean13': fields.related('product_variant_ids', 'ean13', type='char', string='EAN13 Barcode'),
+ 'default_code': fields.related('product_variant_ids', 'default_code', type='char', string='Internal Reference'),
}
+ def _price_get_list_price(self, product):
+ return 0.0
+
+ def _price_get(self, cr, uid, products, ptype='list_price', context=None):
+ if context is None:
+ context = {}
+
+ if 'currency_id' in context:
+ pricetype_obj = self.pool.get('product.price.type')
+ price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
+ price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
+
+ res = {}
+ product_uom_obj = self.pool.get('product.uom')
+ for product in products:
+ res[product.id] = product[ptype] or 0.0
+ if ptype == 'list_price':
+ res[product.id] += product._name == "product.product" and product.price_extra or 0.0
+ if 'uom' in context:
+ uom = product.uom_id or product.uos_id
+ res[product.id] = product_uom_obj._compute_price(cr, uid,
+ uom.id, res[product.id], context['uom'])
+ # Convert from price_type currency to asked one
+ if 'currency_id' in context:
+ # Take the price_type currency from the product field
+ # This is right cause a field cannot be in more than one currency
+ res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
+ context['currency_id'], res[product.id],context=context)
+
+ return res
+
def _get_uom_id(self, cr, uid, *args):
- cr.execute('select id from product_uom order by id limit 1')
- res = cr.fetchone()
- return res and res[0] or False
+ return self.pool["product.uom"].search(cr, uid, [], limit=1, order='id')[0]
def _default_category(self, cr, uid, context=None):
if context is None:
@@ -505,9 +609,68 @@ class product_template(osv.osv):
return {'value': {'uom_po_id': uom_id}}
return {}
+ def create_variant_ids(self, cr, uid, ids, context=None):
+ product_obj = self.pool.get("product.product")
+ ctx = context and context.copy() or {}
+
+ if ctx.get("create_product_variant"):
+ return None
+
+ ctx.update(active_test=False, create_product_variant=True)
+
+ tmpl_ids = self.browse(cr, uid, ids, context=ctx)
+ for tmpl_id in tmpl_ids:
+
+ # list of values combination
+ all_variants = [[]]
+ for variant_id in tmpl_id.attribute_line_ids:
+ if len(variant_id.value_ids) > 1:
+ temp_variants = []
+ for value_id in variant_id.value_ids:
+ for variant in all_variants:
+ temp_variants.append(variant + [int(value_id)])
+ all_variants = temp_variants
+
+ # check product
+ variant_ids_to_active = []
+ variants_active_ids = []
+ variants_inactive = []
+ for product_id in tmpl_id.product_variant_ids:
+ variants = map(int,product_id.attribute_value_ids)
+ if variants in all_variants:
+ variants_active_ids.append(product_id.id)
+ all_variants.pop(all_variants.index(variants))
+ if not product_id.active:
+ variant_ids_to_active.append(product_id.id)
+ else:
+ variants_inactive.append(product_id)
+ if variant_ids_to_active:
+ product_obj.write(cr, uid, variant_ids_to_active, {'active': True}, context=ctx)
+
+ # create new product
+ for variant_ids in all_variants:
+ values = {
+ 'product_tmpl_id': tmpl_id.id,
+ 'attribute_value_ids': [(6, 0, variant_ids)]
+ }
+ id = product_obj.create(cr, uid, values, context=ctx)
+ variants_active_ids.append(id)
+
+ # unlink or inactive product
+ for variant_id in map(int,variants_inactive):
+ try:
+ with cr.savepoint():
+ product_obj.unlink(cr, uid, [variant_id], context=ctx)
+ except (psycopg2.Error, osv.except_osv):
+ product_obj.write(cr, uid, [variant_id], {'active': False}, context=ctx)
+ pass
+ return True
+
def create(self, cr, uid, vals, context=None):
''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
product_template_id = super(product_template, self).create(cr, uid, vals, context=context)
+ if not context or "create_product_product" not in context:
+ self.create_variant_ids(cr, uid, [product_template_id], context=context)
self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
return product_template_id
@@ -524,7 +687,17 @@ class product_template(osv.osv):
if 'standard_price' in vals:
for prod_template_id in ids:
self._set_standard_price(cr, uid, prod_template_id, vals['standard_price'], context=context)
- return super(product_template, self).write(cr, uid, ids, vals, context=context)
+ res = super(product_template, self).write(cr, uid, ids, vals, context=context)
+ if 'attribute_line_ids' in vals or vals.get('active'):
+ self.create_variant_ids(cr, uid, ids, context=context)
+ if 'active' in vals and not vals.get('active'):
+ ctx = context and context.copy() or {}
+ ctx.update(active_test=False)
+ product_ids = []
+ for product in self.browse(cr, uid, ids, context=ctx):
+ product_ids = map(int,product.product_variant_ids)
+ self.pool.get("product.product").write(cr, uid, product_ids, {'active': vals.get('active')}, context=ctx)
+ return res
def copy(self, cr, uid, id, default=None, context=None):
if default is None:
@@ -544,6 +717,7 @@ class product_template(osv.osv):
'mes_type': 'fixed',
'categ_id' : _default_category,
'type' : 'consu',
+ 'active': True,
}
def _check_uom(self, cursor, user, ids, context=None):
@@ -579,14 +753,6 @@ class product_product(osv.osv):
_inherit = ['mail.thread']
_order = 'default_code,name_template'
- def view_header_get(self, cr, uid, view_id, view_type, context=None):
- if context is None:
- context = {}
- res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
- if (context.get('categ_id', False)):
- return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
- return res
-
def _product_price(self, cr, uid, ids, name, arg, context=None):
plobj = self.pool.get('product.pricelist')
res = {}
@@ -613,11 +779,19 @@ class product_product(osv.osv):
res.setdefault(id, 0.0)
return res
+ def view_header_get(self, cr, uid, view_id, view_type, context=None):
+ if context is None:
+ context = {}
+ res = super(product_product, self).view_header_get(cr, uid, view_id, view_type, context)
+ if (context.get('categ_id', False)):
+ return _('Products: ') + self.pool.get('product.category').browse(cr, uid, context['categ_id'], context=context).name
+ return res
+
def _product_lst_price(self, cr, uid, ids, name, arg, context=None):
res = {}
product_uom_obj = self.pool.get('product.uom')
- for id in ids:
- res.setdefault(id, 0.0)
+ res = dict.fromkeys(ids, 0.0)
+
for product in self.browse(cr, uid, ids, context=context):
if 'uom' in context:
uom = product.uos_id or product.uom_id
@@ -625,20 +799,17 @@ class product_product(osv.osv):
uom.id, product.list_price, context['uom'])
else:
res[product.id] = product.list_price
- res[product.id] = (res[product.id] or 0.0) * (product.price_margin or 1.0) + product.price_extra
+ price_extra = 0.0
+ for variant_id in product.attribute_value_ids:
+ price_extra += variant_id.price_extra
+ res[product.id] = (res[product.id] or 0.0) + price_extra
return res
- def _save_product_lst_price(self, cr, uid, product_id, field_name, field_value, arg, context=None):
- field_value = field_value or 0.0
- product = self.browse(cr, uid, product_id, context=context)
- list_price = (field_value - product.price_extra) / (product.price_margin or 1.0)
- return self.write(cr, uid, [product_id], {'list_price': list_price}, context=context)
-
def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
for supinfo in product.seller_ids:
if supinfo.name.id == partner_id:
- return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name, 'variants': ''}
- res = {'code': product.default_code, 'name': product.name, 'variants': product.variants}
+ return {'code': supinfo.product_code or product.default_code, 'name': supinfo.product_name or product.name}
+ res = {'code': product.default_code, 'name': product.name}
return res
def _product_code(self, cr, uid, ids, name, arg, context=None):
@@ -655,48 +826,15 @@ class product_product(osv.osv):
context = {}
for p in self.browse(cr, uid, ids, context=context):
data = self._get_partner_code_name(cr, uid, [], p, context.get('partner_id', None), context=context)
- if not data['variants']:
- data['variants'] = p.variants
if not data['code']:
data['code'] = p.code
if not data['name']:
data['name'] = p.name
- res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + \
- (data['name'] or '') + (data['variants'] and (' - '+data['variants']) or '')
+ res[p.id] = (data['code'] and ('['+data['code']+'] ') or '') + (data['name'] or '')
return res
- def _is_only_child(self, cr, uid, ids, name, arg, context=None):
- res = dict.fromkeys(ids, True)
- for product in self.browse(cr, uid, ids, context=context):
- if product.product_tmpl_id and len(product.product_tmpl_id.product_variant_ids) > 1:
- res[product.id] = False
- return res
-
- def _get_main_product_supplier(self, cr, uid, product, context=None):
- """Determines the main (best) product supplier for ``product``,
- returning the corresponding ``supplierinfo`` record, or False
- if none were found. The default strategy is to select the
- supplier with the highest priority (i.e. smallest sequence).
-
- :param browse_record product: product to supply
- :rtype: product.supplierinfo browse_record or False
- """
- sellers = [(seller_info.sequence, seller_info)
- for seller_info in product.seller_ids or []
- if seller_info and isinstance(seller_info.sequence, (int, long))]
- return sellers and sellers[0][1] or False
-
- def _calc_seller(self, cr, uid, ids, fields, arg, context=None):
- result = {}
- for product in self.browse(cr, uid, ids, context=context):
- main_supplier = self._get_main_product_supplier(cr, uid, product, context=context)
- result[product.id] = {
- 'seller_info_id': main_supplier and main_supplier.id or False,
- 'seller_delay': main_supplier.delay if main_supplier else 1,
- 'seller_qty': main_supplier and main_supplier.qty or 0.0,
- 'seller_id': main_supplier and main_supplier.name.id or False
- }
- return result
+ def is_product_variant(self, cr, uid, ids, name, arg, context=None):
+ return dict.fromkeys(ids, True)
def _get_name_template_ids(self, cr, uid, ids, context=None):
result = set()
@@ -705,39 +843,68 @@ class product_product(osv.osv):
result.add(el)
return list(result)
+ def _get_image_variant(self, cr, uid, ids, name, args, context=None):
+ result = dict.fromkeys(ids, False)
+ for obj in self.browse(cr, uid, ids, context=context):
+ result[obj.id] = obj.image_variant or getattr(obj.product_tmpl_id, name)
+ return result
+
+ def _set_image_variant(self, cr, uid, id, name, value, args, context=None):
+ image = tools.image_resize_image_big(value)
+ res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
+ product = self.browse(cr, uid, id, context=context)
+ if not product.product_tmpl_id.image:
+ product.write({'image_variant': None}, context=context)
+ product.product_tmpl_id.write({'image': image}, context=context)
+ return res
+
+ def _get_price_extra(self, cr, uid, ids, name, args, context=None):
+ result = dict.fromkeys(ids, False)
+ for product in self.browse(cr, uid, ids, context=context):
+ price_extra = 0.0
+ for variant_id in product.attribute_value_ids:
+ for price_id in variant_id.price_ids:
+ if price_id.product_tmpl_id.id == product.product_tmpl_id.id:
+ price_extra += price_id.price_extra
+ result[product.id] = price_extra
+ return result
+
_columns = {
- 'price': fields.function(_product_price, fnct_inv=_save_product_lst_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
- 'lst_price' : fields.function(_product_lst_price, fnct_inv=_save_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
+ 'price': fields.function(_product_price, type='float', string='Price', digits_compute=dp.get_precision('Product Price')),
+ 'price_extra': fields.function(_get_price_extra, type='float', string='Variant Extra Price', help="This is le sum of the extra price of all attributes"),
+ 'lst_price': fields.function(_product_lst_price, type='float', string='Public Price', digits_compute=dp.get_precision('Product Price')),
'code': fields.function(_product_code, type='char', string='Internal Reference'),
'partner_ref' : fields.function(_product_partner_ref, type='char', string='Customer ref'),
'default_code' : fields.char('Internal Reference', select=True),
'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
- 'variants': fields.char('Variants', translate=True),
'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete="cascade", select=True),
- 'is_only_child': fields.function(
- _is_only_child, type='boolean', string='Sole child of the parent template'),
'ean13': fields.char('EAN13 Barcode', size=13, help="International Article Number used for product identification."),
'packaging': fields.one2many('product.packaging', 'product_id', 'Packaging', help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
- 'price_extra': fields.float('Variant Price Extra', digits_compute=dp.get_precision('Product Price'), help="Price Extra: Extra price for the variant on sale price. eg. 200 price extra, 1000 + 200 = 1200."),
- 'price_margin': fields.float('Variant Price Margin', digits_compute=dp.get_precision('Product Price'), help="Price Margin: Margin in percentage amount on sale price for the variant. eg. 10 price margin, 1000 * 1.1 = 1100."),
- 'pricelist_id': fields.dummy(string='Pricelist', relation='product.pricelist', type='many2one'),
'name_template': fields.related('product_tmpl_id', 'name', string="Template Name", type='char', store={
'product.template': (_get_name_template_ids, ['name'], 10),
'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
}, select=True),
- 'color': fields.integer('Color Index'),
- 'seller_info_id': fields.function(_calc_seller, type='many2one', relation="product.supplierinfo", string="Supplier Info", multi="seller_info"),
- 'seller_delay': fields.function(_calc_seller, type='integer', string='Supplier Lead Time', multi="seller_info", help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
- 'seller_qty': fields.function(_calc_seller, type='float', string='Supplier Quantity', multi="seller_info", help="This is minimum quantity to purchase from Main Supplier."),
- 'seller_id': fields.function(_calc_seller, type='many2one', relation="res.partner", string='Main Supplier', help="Main Supplier who has highest priority in Supplier List.", multi="seller_info"),
+ 'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True),
+
+ # image: all image fields are base64 encoded and PIL-supported
+ 'image_variant': fields.binary("Variant Image",
+ help="This field holds the image used as image for the product variant, limited to 1024x1024px."),
+
+ 'image': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
+ string="Big-sized image", type="binary",
+ help="Image of the product variant (Big-sized image of product template if false). It is automatically "\
+ "resized as a 1024x1024px image, with aspect ratio preserved."),
+ 'image_small': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
+ string="Small-sized image", type="binary",
+ help="Image of the product variant (Small-sized image of product template if false)."),
+ 'image_medium': fields.function(_get_image_variant, fnct_inv=_set_image_variant,
+ string="Medium-sized image", type="binary",
+ help="Image of the product variant (Medium-sized image of product template if false)."),
}
_defaults = {
- 'active': lambda *a: 1,
- 'price_extra': lambda *a: 0.0,
- 'price_margin': lambda *a: 1.0,
+ 'active': 1,
'color': 0,
- 'is_only_child': True,
}
def unlink(self, cr, uid, ids, context=None):
@@ -767,8 +934,9 @@ class product_product(osv.osv):
def _check_ean_key(self, cr, uid, ids, context=None):
for product in self.read(cr, uid, ids, ['ean13'], context=context):
- res = check_ean(product['ean13'])
- return res
+ if not check_ean(product['ean13']):
+ return False
+ return True
_constraints = [(_check_ean_key, 'You provided an invalid "EAN13 Barcode" reference. You may use the "Internal Reference" field instead.', ['ean13'])]
@@ -782,13 +950,12 @@ class product_product(osv.osv):
ids = [ids]
if not len(ids):
return []
+
def _name_get(d):
name = d.get('name','')
code = d.get('default_code',False)
if code:
name = '[%s] %s' % (code,name)
- if d.get('variants'):
- name = name + ' - %s' % (d['variants'],)
return (d['id'], name)
partner_id = context.get('partner_id', False)
@@ -800,6 +967,8 @@ class product_product(osv.osv):
result = []
for product in self.browse(cr, SUPERUSER_ID, ids, context=context):
+ variant = ", ".join([v.name for v in product.attribute_value_ids])
+ name = variant and "%s (%s)" % (product.name, variant) or product.name
sellers = []
if partner_id:
sellers = filter(lambda x: x.name.id == partner_id, product.seller_ids)
@@ -807,17 +976,15 @@ class product_product(osv.osv):
for s in sellers:
mydict = {
'id': product.id,
- 'name': s.product_name or product.name,
+ 'name': s.product_name or name,
'default_code': s.product_code or product.default_code,
- 'variants': product.variants
}
result.append(_name_get(mydict))
else:
mydict = {
'id': product.id,
- 'name': product.name,
+ 'name': name,
'default_code': product.default_code,
- 'variants': product.variants
}
result.append(_name_get(mydict))
return result
@@ -855,44 +1022,7 @@ class product_product(osv.osv):
#
def price_get(self, cr, uid, ids, ptype='list_price', context=None):
products = self.browse(cr, uid, ids, context=context)
- return self._price_get(cr, uid, products, ptype=ptype, context=context)
-
- def _price_get(self, cr, uid, products, ptype='list_price', context=None):
- if context is None:
- context = {}
-
- if 'currency_id' in context:
- pricetype_obj = self.pool.get('product.price.type')
- price_type_id = pricetype_obj.search(cr, uid, [('field','=',ptype)])[0]
- price_type_currency_id = pricetype_obj.browse(cr,uid,price_type_id).currency_id.id
-
- res = {}
- # standard_price field can only be seen by users in base.group_user
- # Thus, in order to compute the sale price from the cost price for users not in this group
- # We fetch the standard price as the superuser
- for product in products:
- if ptype != 'standard_price':
- res[product.id] = product[ptype] or 0.0
- else:
- res[product.id] = self.read(cr, SUPERUSER_ID, product.id, [ptype], context=context)[ptype] or 0.0
-
- product_uom_obj = self.pool.get('product.uom')
- for product in products:
- if ptype == 'list_price':
- res[product.id] = (res[product.id] * (product.price_margin or 1.0)) + \
- product.price_extra
- if 'uom' in context:
- uom = product.uom_id or product.uos_id
- res[product.id] = product_uom_obj._compute_price(cr, uid,
- uom.id, res[product.id], context['uom'])
- # Convert from price_type currency to asked one
- if 'currency_id' in context:
- # Take the price_type currency from the product field
- # This is right cause a field cannot be in more than one currency
- res[product.id] = self.pool.get('res.currency').compute(cr, uid, price_type_currency_id,
- context['currency_id'], res[product.id],context=context)
-
- return res
+ return self.pool.get("product.template")._price_get(cr, uid, products, ptype=ptype, context=context)
def copy(self, cr, uid, id, default=None, context=None):
if context is None:
@@ -923,6 +1053,12 @@ class product_product(osv.osv):
'res_id': product.product_tmpl_id.id,
'target': 'new'}
+ def create(self, cr, uid, vals, context=None):
+ if context is None:
+ context = {}
+ ctx = dict(context or {}, create_product_product=True)
+ return super(product_product, self).create(cr, uid, vals, context=ctx)
+
class product_packaging(osv.osv):
_name = "product.packaging"
@@ -948,8 +1084,9 @@ class product_packaging(osv.osv):
def _check_ean_key(self, cr, uid, ids, context=None):
for pack in self.browse(cr, uid, ids, context=context):
- res = check_ean(pack.ean)
- return res
+ if not check_ean(pack.ean):
+ return False
+ return True
_constraints = [(_check_ean_key, 'Error: Invalid ean code', ['ean'])]
@@ -969,8 +1106,8 @@ class product_packaging(osv.osv):
return (res and res[0]) or False
_defaults = {
- 'rows' : lambda *a : 3,
- 'sequence' : lambda *a : 1,
+ 'rows' : 3,
+ 'sequence' : 1,
'ul' : _get_1st_ul,
}
@@ -1010,9 +1147,9 @@ class product_supplierinfo(osv.osv):
'company_id':fields.many2one('res.company','Company',select=1),
}
_defaults = {
- 'qty': lambda *a: 0.0,
- 'sequence': lambda *a: 1,
- 'delay': lambda *a: 1,
+ 'qty': 0.0,
+ 'sequence': 1,
+ 'delay': 1,
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'product.supplierinfo', context=c),
}
def price_get(self, cr, uid, supplier_ids, product_id, product_qty=1, context=None):
diff --git a/addons/product/product_data.xml b/addons/product/product_data.xml
index 85462bb851d..54e6fac5ce2 100644
--- a/addons/product/product_data.xml
+++ b/addons/product/product_data.xml
@@ -255,10 +255,5 @@ parameter) will see those record just disappear.
service
-
-
-
- Others
-
diff --git a/addons/product/product_demo.xml b/addons/product/product_demo.xml
index 20d3777c11f..4f922f40629 100644
--- a/addons/product/product_demo.xml
+++ b/addons/product/product_demo.xml
@@ -56,127 +56,6 @@
box
-
-
-
-
- Computers
-
-
-
-
- Components
-
-
-
-
- Case
-
-
-
- Hard Drive
-
-
-
- Motherboard
-
-
-
- Graphics Card
-
-
-
- Memory
-
-
-
- Processor
-
-
-
- Video Acquisition
-
-
-
-
- Devices
-
-
-
-
- Screen
-
-
-
- Pen Drive
-
-
-
- External Hard Drive
-
-
-
- Keyboard / Mouse
-
-
-
- Printer
-
-
-
- Speakers
-
-
-
- Headset
-
-
-
- Software
-
-
-
-
- Laptops
-
-
-
-
- Computers
-
-
-
-
- Computer all-in-one
-
-
-
- Server
-
-
-
-
- Network
-
-
-
-
- Switch
-
-
-
- Modem & Router
-
-
-
- Switch
-
-
-
-
- Services
-
-
Apple Products
@@ -210,10 +89,9 @@
-
+
On Site Monitoring
-
20.5
30.75
service
@@ -222,15 +100,11 @@
This type of service include basic monitoring of products.
This type of service include basic monitoring of products.
-
-
-
-
+
On Site Assistance
-
25.5
38.25
service
@@ -238,15 +112,11 @@
This type of service include assistance for security questions, system configuration requirements, implementation or special needs.
-
-
-
-
+
PC Assemble SC234
-
450.0
300.0
consu
@@ -256,17 +126,46 @@
Processor AMD 8-Core
512MB RAM
HDD SH-1
-
-
-
PCSC234
+
-
+
+ Memory
+
+
+ 16 Go
+
+
+
+ 32 Go
+
+
+
+
+ Color
+
+
+ White
+
+
+
+ Black
+
+
+
+
+ Wi-Fi
+
+
+ 2.4 GHz
+
+
+
+
iPad Retina Display
-
500.0
750.0
consu
@@ -275,17 +174,56 @@ HDD SH-1
7.9‑inch (diagonal) LED-backlit, 128Gb
Dual-core A5 with quad-core graphics
FaceTime HD Camera, 1.2 MP Photos
-
-
-
A2323
+
+
+
+ A2324
+
+
+
+
+ A2325
+
+
+
+
+ A2326
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ 50.40
+
+
+
+
+
Bose Mini Bluetooth Speaker
-
600.0
147.0
consu
@@ -293,44 +231,33 @@ FaceTime HD Camera, 1.2 MP Photos
Custom computer assembled on order based on customer's requirement.
Bose's smallest portable Bluetooth speaker
-
-
-
B3423
-
+
iPad Mini
-
800.0
320.0
consu
-
-
-
A1232
-
+
Apple In-Ear Headphones
-
70.0
79.0
consu
-
-
-
A8767
-
+
iMac
1299.0
@@ -338,272 +265,215 @@ FaceTime HD Camera, 1.2 MP Photos
consu
-
-
-
A1090
-
+
Apple Wireless Keyboard
-
10.0
47.0
consu
-
-
-
AK789
-
+
Mouse, Optical
-
12.50
14
consu
-
-
-
M-Opt
-
- iPod
-
-
- 14
- 16.50
- consu
-
-
-
+
+
iPod
- 16 Gb
- A6678
-
14
16.50
consu
-
A6678
+
-
- 32 Gb
- 12
-
+
+ A6679
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6.40
+
+
+
+
+
Mouse, Wireless
-
18
12.50
consu
-
-
-
M-Wir
-
+
RAM SR5
-
78.0
85.0
consu
-
-
-
RAM-SR5
-
+
RAM SR2
-
87.0
95.0
consu
-
-
-
RAM-SR2
-
+
RAM SR3
-
80.0
85.0
consu
-
-
-
RAM-SR3
-
+
Computer Case
-
20.0
25.0
consu
-
-
-
C-Case
-
+
HDD SH-1
-
860.0
975.0
consu
-
-
-
HDD-SH1
-
+
HDD SH-2
-
1020.0
1150.0
consu
-
-
-
HDD-SH2
-
+
HDD on Demand
-
1100.0
1250.0
consu
On demand hard-disk having capacity based on requirement.
-
-
-
HDD-DEM
-
+
Motherboard I9P57
-
1700.0
1950.0
consu
-
-
-
MBi9
-
+
Motherboard A20Z7
-
1790.0
2000.0
consu
-
-
-
MBa20
-
+
Processor Core i5 2.70 Ghz
-
2010.0
2100.0
consu
-
-
-
CPUi5
-
+
Processor AMD 8-Core
-
1910.0
1980.0
consu
-
-
-
CPUa8
-
+
Graphics Card
-
876.0
885.0
consu
-
-
-
CARD
-
+
Laptop E5023
-
2870.0
2950.0
consu
@@ -613,16 +483,12 @@ FaceTime HD Camera, 1.2 MP Photos
4GB RAM
Standard-1294P Processor
QWERTY keyboard
-
-
-
LAP-E5
-
+
Laptop S3450
-
3000.0
3245.0
consu
@@ -632,141 +498,106 @@ QWERTY keyboard
6GB RAM
Hi-Speed 234Q Processor
QWERTY keyboard
-
-
-
LAP-S3
-
+
Laptop Customized
-
3300.0
3645.0
consu
Custom Laptop based on customer's requirement.
-
-
-
LAP-CUS
-
+
External Hard disk
-
390.0
405.0
consu
-
-
-
EXT-HDD
-
+
Pen drive, SP-2
-
90.0
100.0
consu
-
-
-
PD-SP2
-
+
Pen drive, SP-4
-
126.0
145.0
consu
-
-
-
PD-SP4
-
+
Multimedia Speakers
-
134.0
150.0
consu
.
-
-
-
MM-SPK
-
+
Headset standard
-
57.0
62.0
consu
Hands free headset for laptop PC with in-line microphone and headphone plug.
-
-
-
HEAD
-
+
Headset USB
-
60.0
65.0
consu
Headset for laptop PC with USB connector.
-
-
-
HEAD-USB
-
+
Webcam
-
38.0
45.0
consu
-
-
-
WCAM
-
+
Blank CD
18.40
@@ -774,14 +605,11 @@ QWERTY keyboard
consu
-
-
-
CD
-
+
Blank DVD-RW
21.60
@@ -789,193 +617,146 @@ QWERTY keyboard
consu
-
-
-
DVD
-
+
Printer, All-in-one
-
4258.0
4410.0
consu
All in one hi-speed printer with fax and scanner.
-
-
-
PRINT
-
+
Ink Cartridge
-
60.0
65.0
consu
-
-
-
INK
-
+
Toner Cartridge
-
66.0
70.0
consu
-
-
-
TONER
-
+
Windows 7 Professional
-
330.0
470.0
consu
-
-
-
Win7
-
+
Windows Home Server 2011
-
540.0
620.0
consu
-
-
-
WServer
-
+
Office Suite
-
110.0
170.0
consu
Office Editing Software with word processing, spreadsheets, presentations, graphics, and databases...
-
-
-
OSuite
-
+
Zed+ Antivirus
-
235.0
280.0
consu
-
-
-
Zplus
-
+
GrapWorks Software
-
155.0
173.0
consu
Full featured image editing software.
-
-
-
GRAPs/w
-
+
Router R430
-
55.0
60.0
consu
-
-
-
ROUT_430
-
+
Datacard
-
35.0
40.0
consu
-
-
-
DC
-
+
Switch, 24 ports
-
55.0
70.0
consu
-
-
-
SW24
-
+
USB Adapter
13.0
@@ -983,9 +764,6 @@ QWERTY keyboard
consu
-
-
-
ADPT
@@ -995,315 +773,315 @@ QWERTY keyboard
-->
-
+
3
1
-
+
3
1
-
+
3
1
-
+
3
1
-
+
2
5
-
+
4
1
-
+
2
1
-
+
2
1
-
+
5
1
-
+
5
1
-
+
5
1
-
+
1
1
-
+
1
1
-
+
3
1
-
+
3
1
-
+
4
5
-
+
3
1
-
+
2
1
-
+
3
1
-
+
3
1
-
+
3
1
-
+
8
1
-
+
8
1
-
+
4
1
-
+
5
1
-
+
2
12
-
+
2
12
-
+
2
5
-
+
2
12
-
+
2
5
-
+
2
1
-
+
2
1
-
+
4
1
-
+
4
1
-
+
5
1
-
+
5
1
-
+
7
1
-
+
4
1
-
+
4
0
-
+
5
0
-
+
2
0
-
+
4
0
-
+
10
0
-
+
3
0
-
+
5
0
diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml
index 6983fe44a12..9b5770a565d 100644
--- a/addons/product/product_view.xml
+++ b/addons/product/product_view.xml
@@ -1,198 +1,140 @@
-
+
-
- product.search.form
- product.product
+
+
+
+ product.template.search
+ product.template
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
-
- product.product.tree
- product.product
-
+
+ product.template.product.tree
+ product.template
-
-
+
-
-
-
-
-
-
-
+
+
-
-
+
-
- product.normal.form
- product.product
-
+
+ product.template.product.form
+ product.template