summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAmit Patel <apa@tinyerp.com>2012-09-14 10:37:33 +0530
committerAmit Patel <apa@tinyerp.com>2012-09-14 10:37:33 +0530
commit4eac135e0859b70d7e87d92478ff5a609eaef6ed (patch)
tree55aaa3d2c0eb1c463a15dff97483a430d5273677
parent23951e1e0d86fe86732928a143c9029e2fcf6862 (diff)
parent9df7381ced04b44e470fe0a060b6b82773f20c84 (diff)
[MERGE]:trunk
bzr revid: apa@tinyerp.com-20120914050733-9euo3vftm8fee6sq
-rw-r--r--addons/account/account.py14
-rw-r--r--addons/account/account_bank.py14
-rw-r--r--addons/account/account_view.xml12
-rw-r--r--addons/account/res_config.py10
-rw-r--r--addons/account/res_config_view.xml6
-rw-r--r--addons/crm/crm_lead.py10
-rw-r--r--addons/email_template/tests/test_mail.py16
-rw-r--r--addons/mail/__openerp__.py1
-rw-r--r--addons/mail/mail_followers.py60
-rw-r--r--addons/mail/mail_group.py16
-rw-r--r--addons/mail/mail_mail.py114
-rw-r--r--addons/mail/mail_message.py30
-rw-r--r--addons/mail/mail_thread.py20
-rw-r--r--addons/mail/res_users.py3
-rw-r--r--addons/mail/static/src/css/mail.css4
-rw-r--r--addons/mail/static/src/js/mail_followers.js57
-rw-r--r--addons/mail/static/src/xml/mail.xml6
-rw-r--r--addons/mail/static/src/xml/mail_followers.xml3
-rw-r--r--addons/mail/tests/test_mail.py78
-rw-r--r--addons/mail/wizard/__init__.py1
-rw-r--r--addons/mail/wizard/invite.py82
-rw-r--r--addons/mail/wizard/invite_view.xml29
-rw-r--r--addons/mail/wizard/mail_compose_message.py20
-rw-r--r--addons/point_of_sale/controllers/main.py13
-rw-r--r--addons/point_of_sale/point_of_sale_demo.xml7
-rw-r--r--addons/point_of_sale/static/src/css/keyboard.css24
-rw-r--r--addons/point_of_sale/static/src/css/pos.css85
-rw-r--r--addons/point_of_sale/static/src/js/db.js25
-rw-r--r--addons/point_of_sale/static/src/js/devices.js131
-rw-r--r--addons/point_of_sale/static/src/js/models.js65
-rw-r--r--addons/point_of_sale/static/src/js/screens.js88
-rw-r--r--addons/point_of_sale/static/src/js/widgets.js123
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml64
-rw-r--r--addons/portal/__init__.py1
-rw-r--r--addons/portal/mail_mail.py52
-rw-r--r--addons/portal/tests/__init__.py27
-rw-r--r--addons/portal/tests/test_portal.py81
-rw-r--r--addons/sale/wizard/sale_make_invoice_advance.py2
-rw-r--r--addons/web_linkedin/static/src/js/linkedin.js4
39 files changed, 1062 insertions, 336 deletions
diff --git a/addons/account/account.py b/addons/account/account.py
index e5e29f178a3..d4e31167c43 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -2338,6 +2338,16 @@ class account_model(osv.osv):
return move_ids
+ def onchange_journal_id(self, cr, uid, ids, journal_id, context=None):
+ company_id = False
+
+ if journal_id:
+ journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
+ if journal.company_id.id:
+ company_id = journal.company_id.id
+
+ return {'value': {'company_id': company_id}}
+
account_model()
class account_model_line(osv.osv):
@@ -3013,9 +3023,9 @@ class wizard_multi_charts_accounts(osv.osv_memory):
'purchase_tax_rate': fields.float('Purchase Tax(%)'),
'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete'),
}
-
+
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
- currency_id = False
+ currency_id = False
if company_id:
currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id
return {'value': {'currency_id': currency_id}}
diff --git a/addons/account/account_bank.py b/addons/account/account_bank.py
index ef146128a67..e44d2acd163 100644
--- a/addons/account/account_bank.py
+++ b/addons/account/account_bank.py
@@ -43,11 +43,15 @@ class bank(osv.osv):
"Return the name to use when creating a bank journal"
return (bank.bank_name or '') + ' ' + bank.acc_number
- def _prepare_name_get(self, cr, uid, bank_type_obj, bank_obj, context=None):
- """Add ability to have %(currency_name)s in the format_layout of
- res.partner.bank.type"""
- bank_obj._data[bank_obj.id]['currency_name'] = bank_obj.currency_id and bank_obj.currency_id.name or ''
- return super(bank, self)._prepare_name_get(cr, uid, bank_type_obj, bank_obj, context=context)
+ def _prepare_name_get(self, cr, uid, bank_dicts, context=None):
+ """Add ability to have %(currency_name)s in the format_layout of res.partner.bank.type"""
+ currency_ids = list(set(data['currency_id'][0] for data in bank_dicts if data['currency_id']))
+ currencies = self.pool.get('res.currency').browse(cr, uid, currency_ids, context=context)
+ currency_name = dict((currency.id, currency.name) for currency in currencies)
+
+ for data in bank_dicts:
+ data['currency_name'] = data['currency_id'] and currency_name[data['currency_id'][0]] or ''
+ return super(bank, self)._prepare_name_get(cr, uid, bank_dicts, context=context)
def post_write(self, cr, uid, ids, context={}):
if isinstance(ids, (int, long)):
diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml
index 8c0de73ef8c..d1b19910365 100644
--- a/addons/account/account_view.xml
+++ b/addons/account/account_view.xml
@@ -112,7 +112,7 @@
<field name="fiscalyear_id" widget="selection"/>
<label for="date_start" string="Duration"/>
<div>
- <field name="date_start" class="oe_inline" nolabel="1"/> -
+ <field name="date_start" class="oe_inline" nolabel="1"/> -
<field name="date_stop" nolabel="1" class="oe_inline"/>
</div>
</group>
@@ -181,7 +181,7 @@
<form string="Account" version="7.0">
<label for="code" class="oe_edit_only" string="Account Code and Name"/>
<h1>
- <field name="code" class="oe_inline" placeholder="Account code" style="width: 6em"/> -
+ <field name="code" class="oe_inline" placeholder="Account code" style="width: 6em"/> -
<field name="name" class="oe_inline" placeholder="Account name"/>
</h1>
<group>
@@ -1082,7 +1082,7 @@
<field eval="2" name="priority"/>
<field name="arch" type="xml">
<form string="Journal Item" version="7.0">
- <sheet>
+ <sheet>
<group>
<group>
<field name="name"/>
@@ -1349,7 +1349,7 @@
<field name="date"/>
<field name="to_check"/>
<field name="amount" invisible="1"/>
- </group>
+ </group>
</group>
<notebook>
<page string="Journal Items">
@@ -1651,8 +1651,8 @@
<form string="Journal Entry Model" version="7.0">
<group col="4">
<field name="name"/>
- <field name="journal_id"/>
- <field name="company_id" widget='selection' groups="base.group_multi_company"/>
+ <field name="journal_id" on_change="onchange_journal_id(journal_id)"/>
+ <field name="company_id" widget="selection" groups="base.group_multi_company"/>
</group>
<field name="lines_id" widget="one2many_list"/>
diff --git a/addons/account/res_config.py b/addons/account/res_config.py
index 91f1068d166..ee7eb17777f 100644
--- a/addons/account/res_config.py
+++ b/addons/account/res_config.py
@@ -223,6 +223,16 @@ class account_config_settings(osv.osv_memory):
return {'value': {'date_stop': end_date.strftime('%Y-%m-%d')}}
return {}
+ def open_company_form(self, cr, uid, ids, context=None):
+ config = self.browse(cr, uid, ids[0], context)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Configure your Company',
+ 'res_model': 'res.company',
+ 'res_id': config.company_id.id,
+ 'view_mode': 'form',
+ }
+
def set_default_taxes(self, cr, uid, ids, context=None):
""" set default sale and purchase taxes for products """
ir_values = self.pool.get('ir.values')
diff --git a/addons/account/res_config_view.xml b/addons/account/res_config_view.xml
index bc8ace5c262..d7618fbff5a 100644
--- a/addons/account/res_config_view.xml
+++ b/addons/account/res_config_view.xml
@@ -224,10 +224,8 @@
<div>
<div>
<label for="company_footer"/>
- <button name="%(action_bank_tree)d"
- string="Configure your bank accounts"
- icon="gtk-go-forward"
- type="action"
+ <button name="open_company_form" type="object"
+ string="Configure your company bank accounts" icon="gtk-go-forward"
class="oe_inline oe_link"/>
<field name="company_footer"/>
</div>
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index ddfdb05ca66..fff47205582 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -27,13 +27,15 @@ import time
import tools
from tools.translate import _
+from base.res.res_partner import format_address
+
CRM_LEAD_PENDING_STATES = (
crm.AVAILABLE_STATES[2][0], # Cancelled
crm.AVAILABLE_STATES[3][0], # Done
crm.AVAILABLE_STATES[4][0], # Pending
)
-class crm_lead(base_stage, osv.osv):
+class crm_lead(base_stage, format_address, osv.osv):
""" CRM Lead Case """
_name = "crm.lead"
_description = "Lead/Opportunity"
@@ -105,6 +107,12 @@ class crm_lead(base_stage, osv.osv):
return result, fold
+ def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
+ res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
+ if view_type == 'form':
+ res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
+ return res
+
_group_by_full = {
'stage_id': _read_group_stage_ids
}
diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py
index 85ab3afc902..1091c34ad5c 100644
--- a/addons/email_template/tests/test_mail.py
+++ b/addons/email_template/tests/test_mail.py
@@ -20,17 +20,10 @@
##############################################################################
import base64
-from openerp.tests import common
+from openerp.addons.mail.tests import test_mail
-class test_message_compose(common.TransactionCase):
- def _mock_smtp_gateway(self, *args, **kwargs):
- return True
-
- def _mock_build_email(self, *args, **kwargs):
- self._build_email_args = args
- self._build_email_kwargs = kwargs
- return self.build_email_real(*args, **kwargs)
+class test_message_compose(test_mail.TestMailMockups):
def setUp(self):
super(test_message_compose, self).setUp()
@@ -40,11 +33,6 @@ class test_message_compose(common.TransactionCase):
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
- # Install mock SMTP gateway
- self.build_email_real = self.registry('ir.mail_server').build_email
- self.registry('ir.mail_server').build_email = self._mock_build_email
- self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
-
# create a 'pigs' and 'bird' groups that will be used through the various tests
self.group_pigs_id = self.mail_group.create(self.cr, self.uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py
index 08a98cb0a02..f0ab2c67bcf 100644
--- a/addons/mail/__openerp__.py
+++ b/addons/mail/__openerp__.py
@@ -61,6 +61,7 @@ The main features of the module are:
'website': 'http://www.openerp.com',
'depends': ['base', 'base_tools', 'base_setup'],
'data': [
+ 'wizard/invite_view.xml',
'wizard/mail_compose_message_view.xml',
'res_config_view.xml',
'mail_message_view.xml',
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index 2aba685c4fa..b8f6c21ecf5 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -23,6 +23,7 @@ from osv import osv
from osv import fields
import tools
+
class mail_followers(osv.Model):
""" mail_followers holds the data related to the follow mechanism inside
OpenERP. Partners can choose to follow documents (records) of any kind
@@ -84,16 +85,44 @@ class mail_notification(osv.Model):
notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
return self.write(cr, uid, notif_ids, {'read': True}, context=context)
+ def get_partners_to_notify(self, cr, uid, partner_ids, message, context=None):
+ """ Return the list of partners to notify, based on their preferences.
+
+ :param browse_record message: mail.message to notify
+ """
+ notify_pids = []
+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+ # Do not send an email to the writer
+ if partner.user_ids and partner.user_ids[0].id == uid:
+ continue
+ # Do not send to partners without email address defined
+ if not partner.email:
+ continue
+ # Partner does not want to receive any emails
+ if partner.notification_email_send == 'none':
+ continue
+ # Partner wants to receive only emails and comments
+ if partner.notification_email_send == 'comment' and message.type not in ('email', 'comment'):
+ continue
+ # Partner wants to receive only emails
+ if partner.notification_email_send == 'email' and message.type != 'email':
+ continue
+ notify_pids.append(partner.id)
+ return notify_pids
+
def notify(self, cr, uid, partner_ids, msg_id, context=None):
""" Send by email the notification depending on the user preferences """
context = context or {}
# mail_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_noemail') or not partner_ids:
return True
-
- mail_mail = self.pool.get('mail.mail')
msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
+ notify_partner_ids = self.get_partners_to_notify(cr, uid, partner_ids, msg, context=context)
+ if not notify_partner_ids:
+ return True
+
+ mail_mail = self.pool.get('mail.mail')
# add signature
body_html = msg.body
signature = msg.author_id and msg.author_id.user_ids[0].signature or ''
@@ -107,27 +136,6 @@ class mail_notification(osv.Model):
'body_html': body_html,
'state': 'outgoing',
}
-
- for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
- # Do not send an email to the writer
- if partner.user_ids and partner.user_ids[0].id == uid:
- continue
- # Do not send to partners without email address defined
- if not partner.email:
- continue
- # Partner does not want to receive any emails
- if partner.notification_email_send == 'none':
- continue
- # Partner wants to receive only emails and comments
- if partner.notification_email_send == 'comment' and msg.type not in ('email', 'comment'):
- continue
- # Partner wants to receive only emails
- if partner.notification_email_send == 'email' and msg.type != 'email':
- continue
- if partner.email not in mail_values['email_to']:
- mail_values['email_to'].append(partner.email)
- if mail_values['email_to']:
- mail_values['email_to'] = ', '.join(mail_values['email_to'])
- email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
- mail_mail.send(cr, uid, [email_notif_id], context=context)
- return True
+ mail_values['email_to'] = ', '.join(mail_values['email_to'])
+ email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
+ return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context)
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 175ce650f06..fe0cc3950ae 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -19,13 +19,11 @@
#
##############################################################################
-import datetime as DT
import openerp
import openerp.tools as tools
-from operator import itemgetter
from osv import osv
from osv import fields
-from tools.translate import _
+
class mail_group(osv.Model):
""" A mail_group is a collection of users sharing messages in a discussion
@@ -47,7 +45,7 @@ class mail_group(osv.Model):
_columns = {
'description': fields.text('Description'),
'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"),
- 'public': fields.selection([('public','Public'),('private','Private'),('groups','Selected Group Only')], 'Privacy', required=True,
+ 'public': fields.selection([('public', 'Public'), ('private', 'Private'), ('groups', 'Selected Group Only')], 'Privacy', required=True,
help='This group is visible by non members. \
Invisible groups can add members through the invite button.'),
'group_public_id': fields.many2one('res.groups', string='Authorized Group'),
@@ -126,14 +124,14 @@ class mail_group(osv.Model):
search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search')
params = {
'search_view_id': search_ref and search_ref[1] or False,
- 'domain': [('model','=','mail.group'), ('res_id','=',mail_group_id)],
+ 'domain': [('model', '=', 'mail.group'), ('res_id', '=', mail_group_id)],
'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id},
'res_model': 'mail.message',
'thread_level': 1,
}
cobj = self.pool.get('ir.actions.client')
newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context)
- self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context)
+ self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,' + str(newref), 'mail_group_id': mail_group_id}, context=context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
@@ -142,9 +140,13 @@ class mail_group(osv.Model):
return mail_group_id
def unlink(self, cr, uid, ids, context=None):
+ groups = self.browse(cr, uid, ids, context=context)
# Cascade-delete mail aliases as well, as they should not exist without the mail group.
mail_alias = self.pool.get('mail.alias')
- alias_ids = [group.alias_id.id for group in self.browse(cr, uid, ids, context=context) if group.alias_id]
+ alias_ids = [group.alias_id.id for group in groups if group.alias_id]
+ # Cascade-delete menu entries as well
+ self.pool.get('ir.ui.menu').unlink(cr, uid, [group.menu_id.id for group in groups if group.menu_id], context=context)
+ # Delete mail_group
res = super(mail_group, self).unlink(cr, uid, ids, context=context)
mail_alias.unlink(cr, uid, alias_ids, context=context)
return res
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index 7b593c5b998..6bbfa9bc30e 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -30,6 +30,7 @@ from tools.translate import _
_logger = logging.getLogger(__name__)
+
class mail_mail(osv.Model):
""" Model holding RFC2822 email messages to send. This model also provides
facilities to queue and send new email messages. """
@@ -58,8 +59,8 @@ class mail_mail(osv.Model):
'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
- # and during unlink() we will cascade delete the parent and its attachments
- 'notification': fields.boolean('Is Notification')
+ # and during unlink() we will not cascade delete the parent and its attachments
+ 'notification': fields.boolean('Is Notification')
}
def _get_default_from(self, cr, uid, context=None):
@@ -76,21 +77,21 @@ class mail_mail(osv.Model):
def create(self, cr, uid, values, context=None):
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
- return super(mail_mail,self).create(cr, uid, values, context=context)
+ return super(mail_mail, self).create(cr, uid, values, context=context)
def unlink(self, cr, uid, ids, context=None):
# cascade-delete the parent message for all mails that are not created for a notification
- ids_to_cascade = self.search(cr, uid, [('notification','=',False),('id','in',ids)])
+ ids_to_cascade = self.search(cr, uid, [('notification', '=', False), ('id', 'in', ids)])
parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
- res = super(mail_mail,self).unlink(cr, uid, ids, context=context)
+ res = super(mail_mail, self).unlink(cr, uid, ids, context=context)
self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
return res
def mark_outgoing(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
+ return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
def cancel(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
+ return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
def process_email_queue(self, cr, uid, ids=None, context=None):
"""Send immediately queued messages, committing after each
@@ -127,7 +128,7 @@ class mail_mail(osv.Model):
"""Perform any post-processing necessary after sending ``mail``
successfully, including deleting it completely along with its
attachment if the ``auto_delete`` flag of the mail was set.
- Overridden by subclasses for extra post-processing behaviors.
+ Overridden by subclasses for extra post-processing behaviors.
:param browse_record mail: the mail that was just sent
:return: True
@@ -136,14 +137,46 @@ class mail_mail(osv.Model):
mail.unlink()
return True
- def _send_get_mail_subject(self, cr, uid, mail, force=False, context=None):
- """ if void and related document: '<Author> posted on <Resource>'
- :param mail: mail.mail browse_record """
+ def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
+ """ If subject is void and record_name defined: '<Author> posted on <Resource>'
+
+ :param boolean force: force the subject replacement
+ :param browse_record mail: mail.mail browse_record
+ :param browse_record partner: specific recipient partner
+ """
if force or (not mail.subject and mail.model and mail.res_id):
return '%s posted on %s' % (mail.author_id.name, mail.record_name)
return mail.subject
- def send(self, cr, uid, ids, auto_commit=False, context=None):
+ def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
+ """ Return a specific ir_email body. The main purpose of this method
+ is to be inherited by Portal, to add a link for signing in, in
+ each notification email a partner receives.
+
+ :param browse_record mail: mail.mail browse_record
+ :param browse_record partner: specific recipient partner
+ """
+ return mail.body_html
+
+ def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
+ """ Return a dictionary for specific email values, depending on a
+ partner, or generic to the whole recipients given by mail.email_to.
+
+ :param browse_record mail: mail.mail browse_record
+ :param browse_record partner: specific recipient partner
+ """
+ body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
+ subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
+ body_alternative = tools.html2plaintext(body)
+ email_to = [partner.email] if partner else tools.email_split(mail.email_to)
+ return {
+ 'body': body,
+ 'body_alternative': body_alternative,
+ 'subject': subject,
+ 'email_to': email_to,
+ }
+
+ def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None):
""" Sends the selected emails immediately, ignoring their current
state (mails that have already been sent should not be passed
unless they should actually be re-sent).
@@ -154,50 +187,55 @@ class mail_mail(osv.Model):
:param bool auto_commit: whether to force a commit of the mail status
after sending each mail (meant only for scheduler processing);
should never be True during normal transactions (default: False)
+ :param list recipient_ids: specific list of res.partner recipients.
+ If set, one email is sent to each partner. Its is possible to
+ tune the sent email through ``send_get_mail_body`` and ``send_get_mail_subject``.
+ If not specified, one email is sent to mail_mail.email_to.
:return: True
"""
ir_mail_server = self.pool.get('ir.mail_server')
for mail in self.browse(cr, uid, ids, context=context):
try:
- body = mail.body_html
- subject = self._send_get_mail_subject(cr, uid, mail, context=context)
-
# handle attachments
attachments = []
for attach in mail.attachment_ids:
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
-
- # use only sanitized html and set its plaintexted version as alternative
- body_alternative = tools.html2plaintext(body)
- content_subtype_alternative = 'plain'
+ # specific behavior to customize the send email for notified partners
+ email_list = []
+ if recipient_ids:
+ for partner in self.pool.get('res.partner').browse(cr, uid, recipient_ids, context=context):
+ email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
+ else:
+ email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
# build an RFC2822 email.message.Message object and send it without queuing
- msg = ir_mail_server.build_email(
- email_from = mail.email_from,
- email_to = tools.email_split(mail.email_to),
- subject = subject,
- body = body,
- body_alternative = body_alternative,
- email_cc = tools.email_split(mail.email_cc),
- reply_to = mail.reply_to,
- attachments = attachments,
- message_id = mail.message_id,
- references = mail.references,
- object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
- subtype = 'html',
- subtype_alternative = content_subtype_alternative)
- res = ir_mail_server.send_email(cr, uid, msg,
- mail_server_id=mail.mail_server_id.id, context=context)
+ for email in email_list:
+ msg = ir_mail_server.build_email(
+ email_from = mail.email_from,
+ email_to = email.get('email_to'),
+ subject = email.get('subject'),
+ body = email.get('body'),
+ body_alternative = email.get('body_alternative'),
+ email_cc = tools.email_split(mail.email_cc),
+ reply_to = mail.reply_to,
+ attachments = attachments,
+ message_id = mail.message_id,
+ references = mail.references,
+ object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
+ subtype = 'html',
+ subtype_alternative = 'plain')
+ res = ir_mail_server.send_email(cr, uid, msg,
+ mail_server_id=mail.mail_server_id.id, context=context)
if res:
- mail.write({'state':'sent', 'message_id': res})
+ mail.write({'state': 'sent', 'message_id': res})
else:
- mail.write({'state':'exception'})
+ mail.write({'state': 'exception'})
mail.refresh()
if mail.state == 'sent':
self._postprocess_sent_message(cr, uid, mail, context=context)
except Exception:
_logger.exception('failed sending mail.mail %s', mail.id)
- mail.write({'state':'exception'})
+ mail.write({'state': 'exception'})
if auto_commit == True:
cr.commit()
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 18a88bb0b61..44ce76b94b0 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -20,11 +20,13 @@
##############################################################################
import logging
+import openerp
import tools
from email.header import decode_header
from operator import itemgetter
from osv import osv, fields
+from tools.translate import _
_logger = logging.getLogger(__name__)
@@ -35,6 +37,7 @@ def decode(text):
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
+
class mail_message(osv.Model):
""" Messages model: system notification (replacing res.log notifications),
comments (OpenChatter discussion) and incoming emails. """
@@ -57,7 +60,10 @@ class mail_message(osv.Model):
for message in self.browse(cr, uid, ids, context=context):
if not message.model or not message.res_id:
continue
- result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
+ try:
+ result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
+ except openerp.exceptions.AccessDenied, e:
+ pass
return result
def _get_unread(self, cr, uid, ids, name, arg, context=None):
@@ -341,3 +347,25 @@ class mail_message(osv.Model):
default = {}
default.update(message_id=False, headers=False)
return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
+
+ #------------------------------------------------------
+ # Tools
+ #------------------------------------------------------
+
+ def check_partners_email(self, cr, uid, partner_ids, context=None):
+ """ Verify that selected partner_ids have an email_address defined.
+ Otherwise throw a warning. """
+ partner_wo_email_lst = []
+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+ if not partner.email:
+ partner_wo_email_lst.append(partner)
+ if not partner_wo_email_lst:
+ return {}
+ warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
+ for partner in partner_wo_email_lst:
+ warning_msg += '\n- %s' % (partner.name)
+ return {'warning': {
+ 'title': _('Partners email addresses not found'),
+ 'message': warning_msg,
+ }
+ }
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index c0d6ac90fe6..d798007275f 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -623,14 +623,8 @@ class mail_thread(osv.AbstractModel):
return self.message_subscribe(cr, uid, ids, partner_ids, context=context)
def message_subscribe(self, cr, uid, ids, partner_ids, context=None):
- """ Add partners to the records followers.
- :param partner_ids: a list of partner_ids to subscribe
- :param return: new value of followers if read_back key in context
- """
- self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
- if context and context.get('read_back'):
- return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
- return []
+ """ Add partners to the records followers. """
+ return self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None):
""" Wrapper on message_subscribe, using users. If user_ids is not
@@ -640,14 +634,8 @@ class mail_thread(osv.AbstractModel):
return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context)
def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
- """ Remove partners from the records followers.
- :param partner_ids: a list of partner_ids to unsubscribe
- :param return: new value of followers if read_back key in context
- """
- self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
- if context and context.get('read_back'):
- return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
- return []
+ """ Remove partners from the records followers. """
+ return self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
#------------------------------------------------------
# Thread state
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index f9ca54c478d..8bbe075f159 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -72,6 +72,9 @@ class res_users(osv.Model):
def create(self, cr, uid, data, context=None):
# create default alias same as the login
+ if not data.get('login', False):
+ raise osv.except_osv(_('Invalid Action!'), _('You may not create a user.'))
+
mail_alias = self.pool.get('mail.alias')
alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context)
data['alias_id'] = alias_id
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css
index 92ead6f2432..48a178732b0 100644
--- a/addons/mail/static/src/css/mail.css
+++ b/addons/mail/static/src/css/mail.css
@@ -96,10 +96,6 @@
width: 120px;
}
-.openerp button.oe_mail_button_followers {
- display: inline;
-}
-
.openerp button.oe_mail_button_mouseout {
color: white;
background-color: #8a89ba;
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 34976269285..cf33bd64640 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -31,33 +31,52 @@ openerp_mail_followers = function(session, mail) {
},
start: function() {
- var self = this;
- // NB: all the widget should be modified to check the actual_mode property on view, not use
- // any other method to know if the view is in create mode anymore
+ // use actual_mode property on view to know if the view is in create mode anymore
this.view.on("change:actual_mode", this, this._check_visibility);
this._check_visibility();
- this.$el.find('button.oe_mail_button_follow').click(function () { self.do_follow(); })
- .mouseover(function () { $(this).html('Follow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
- .mouseleave(function () { $(this).html('Not following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
- this.$el.find('button.oe_mail_button_unfollow').click(function () { self.do_unfollow(); })
- .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
- .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
this.reinit();
+ this.bind_events();
},
_check_visibility: function() {
this.$el.toggle(this.view.get("actual_mode") !== "create");
},
- destroy: function () {
- this._super.apply(this, arguments);
- },
-
reinit: function() {
this.$el.find('button.oe_mail_button_follow').hide();
this.$el.find('button.oe_mail_button_unfollow').hide();
},
+ bind_events: function() {
+ var self = this;
+ this.$('button.oe_mail_button_unfollow').on('click', function () { self.do_unfollow(); })
+ .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
+ .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
+ this.$el.on('click', 'button.oe_mail_button_follow', function () { self.do_follow(); });
+ this.$el.on('click', 'button.oe_mail_button_invite', function(event) {
+ action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.wizard.invite',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ 'default_res_model': self.view.dataset.model,
+ 'default_res_id': self.view.datarecord.id
+ },
+ }
+ self.do_action(action, function() { self.read_value(); });
+ });
+ },
+
+ read_value: function() {
+ var self = this;
+ return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).pipe(function (results) {
+ return results[0].message_follower_ids;
+ }).pipe(this.proxy('set_value'));
+ },
+
set_value: function(value_) {
this.reinit();
if (! this.view.datarecord.id ||
@@ -65,11 +84,11 @@ openerp_mail_followers = function(session, mail) {
this.$el.find('div.oe_mail_recthread_aside').hide();
return;
}
- return this.fetch_followers(value_);
+ return this.fetch_followers(value_ || this.get_value());
},
fetch_followers: function (value_) {
- return this.ds_follow.call('read', [value_ || this.get_value(), ['name', 'user_ids']]).then(this.proxy('display_followers'));
+ return this.ds_follow.call('read', [value_, ['name', 'user_ids']]).pipe(this.proxy('display_followers'));
},
/** Display the followers, evaluate is_follower directly */
@@ -91,13 +110,13 @@ openerp_mail_followers = function(session, mail) {
},
do_follow: function () {
- var context = new session.web.CompoundContext(this.build_context(), {'read_back': true});
- return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('set_value'));
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value'));
},
do_unfollow: function () {
- var context = new session.web.CompoundContext(this.build_context(), {'read_back': true});
- return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('set_value'));
+ var context = new session.web.CompoundContext(this.build_context(), {});
+ return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value'));
},
});
};
diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml
index 4d3b56cf62f..bf339f21f1c 100644
--- a/addons/mail/static/src/xml/mail.xml
+++ b/addons/mail/static/src/xml/mail.xml
@@ -114,7 +114,7 @@
<!-- dropdown menu with message options and actions -->
<span class="oe_dropdown_toggle oe_dropdown_arrow">
<ul class="oe_dropdown_menu">
- <li t-if="record.is_author &amp; options.show_dd_delete"><a class="oe_mail_msg_delete" t-attf-data-id='{record.id}'>Delete</a></li>
+ <li t-if="record.is_author and options.show_dd_delete"><a class="oe_mail_msg_delete" t-attf-data-id='{record.id}'>Delete</a></li>
<li t-if="options.show_dd_hide"><a class="oe_mail_msg_hide" t-attf-data-id='{record.id}'>Remove notification</a></li>
<!-- Uncomment when adding subtype hiding
<li t-if="display['show_hide']">
@@ -130,14 +130,14 @@
<t t-raw="record.subject"/>
</h1>
<div class="oe_mail_msg_body">
- <t t-if="options.show_record_name &amp; (!record.subject) &amp; (options.thread_level > 0)">
+ <t t-if="options.show_record_name and record.record_name and (!record.subject) and (options.thread_level > 0)">
<a t-attf-href="#model=#{record.model}&amp;id=#{record.res_id}"><t t-raw="record.record_name"/></a>
</t>
<t t-raw="record.body"/>
</div>
<div class="oe_clear"/>
<ul class="oe_mail_msg_footer">
- <li t-if="options.show_record_name &amp; record.subject &amp; options.thread_level > 0">
+ <li t-if="options.show_record_name and record.record_name and record.subject and options.thread_level > 0">
<a t-attf-href="#model=#{record.model}&amp;id=#{record.res_id}"><t t-raw="record.record_name"/></a>
</li>
<li><a t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"><t t-raw="record.author_id[1]"/></a></li>
diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml
index 39622f42abf..32ce34d48d1 100644
--- a/addons/mail/static/src/xml/mail_followers.xml
+++ b/addons/mail/static/src/xml/mail_followers.xml
@@ -7,7 +7,8 @@
-->
<div t-name="mail.followers" class="oe_mail_recthread_aside oe_semantic_html_override">
<div class="oe_mail_recthread_actions">
- <button type="button" class="oe_mail_button_follow oe_mail_button_mouseout">Not following</button>
+ <button type="button" class="oe_mail_button_invite">Invite</button>
+ <button type="button" class="oe_mail_button_follow">Follow</button>
<button type="button" class="oe_mail_button_unfollow oe_mail_button_mouseout">Following</button>
</div>
<div class="oe_mail_recthread_followers">
diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py
index 069e1b0d35a..1fab5dd1732 100644
--- a/addons/mail/tests/test_mail.py
+++ b/addons/mail/tests/test_mail.py
@@ -19,6 +19,8 @@
#
##############################################################################
+import tools
+
from openerp.tests import common
from openerp.tools.html_sanitize import html_sanitize
@@ -82,15 +84,42 @@ Sylvie
"""
-class test_mail(common.TransactionCase):
+class TestMailMockups(common.TransactionCase):
def _mock_smtp_gateway(self, *args, **kwargs):
return True
+ def _init_mock_build_email(self):
+ self._build_email_args_list = []
+ self._build_email_kwargs_list = []
+
def _mock_build_email(self, *args, **kwargs):
- self._build_email_args = args
- self._build_email_kwargs = kwargs
- return self.build_email_real(*args, **kwargs)
+ self._build_email_args_list.append(args)
+ self._build_email_kwargs_list.append(kwargs)
+ return self._build_email(*args, **kwargs)
+
+ def setUp(self):
+ super(TestMailMockups, self).setUp()
+ # Install mock SMTP gateway
+ self._init_mock_build_email()
+ self._build_email = self.registry('ir.mail_server').build_email
+ self.registry('ir.mail_server').build_email = self._mock_build_email
+ self._send_email = self.registry('ir.mail_server').send_email
+ self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
+
+ def tearDown(self):
+ # Remove mocks
+ self.registry('ir.mail_server').build_email = self._build_email
+ self.registry('ir.mail_server').send_email = self._send_email
+ super(TestMailMockups, self).tearDown()
+
+
+class test_mail(TestMailMockups):
+
+ def _mock_send_get_mail_body(self, *args, **kwargs):
+ # def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None)
+ body = tools.append_content_to_html(args[2].body_html, kwargs.get('partner').name if kwargs.get('partner') else 'No specific partner')
+ return body
def setUp(self):
super(test_mail, self).setUp()
@@ -105,10 +134,9 @@ class test_mail(common.TransactionCase):
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
- # Install mock SMTP gateway
- self.build_email_real = self.registry('ir.mail_server').build_email
- self.registry('ir.mail_server').build_email = self._mock_build_email
- self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
+ # Mock send_get_mail_body to test its functionality without other addons override
+ self._send_get_mail_body = self.registry('mail.mail').send_get_mail_body
+ self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body
# groups@.. will cause the creation of new mail groups
self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model', '=', 'mail.group')])[0]
@@ -118,6 +146,11 @@ class test_mail(common.TransactionCase):
self.group_pigs_id = self.mail_group.create(self.cr, self.uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
+ def tearDown(self):
+ # Remove mocks
+ self.registry('mail.mail').send_get_mail_body = self._send_get_mail_body
+ super(test_mail, self).tearDown()
+
def test_00_message_process(self):
cr, uid = self.cr, self.uid
# Incoming mail creates a new mail_group "frogs"
@@ -274,18 +307,20 @@ class test_mail(common.TransactionCase):
_attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
# CASE1: post comment, body and subject specified
+ self._init_mock_build_email()
msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment')
message = self.mail_message.browse(cr, uid, msg_id)
- sent_email = self._build_email_kwargs
+ sent_emails = self._build_email_kwargs_list
# Test: notifications have been deleted
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id)]), 'mail.mail notifications should have been auto-deleted!')
# Test: mail_message: subject is _subject, body is _body1 (no formatting done)
self.assertEqual(message.subject, _subject, 'mail.message subject incorrect')
self.assertEqual(message.body, _body1, 'mail.message body incorrect')
- # Test: sent_email: email send by server: correct subject, body; body_alternative
- self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
- self.assertEqual(sent_email['body'], _mail_body1, 'sent_email body incorrect')
- self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1, 'sent_email body_alternative is incorrect')
+ # Test: sent_email: email send by server: correct subject, body, body_alternative
+ for sent_email in sent_emails:
+ self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
+ self.assertEqual(sent_email['body'], _mail_body1 + '\n<pre>Bert Tartopoils</pre>\n', 'sent_email body incorrect')
+ self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils', 'sent_email body_alternative is incorrect')
# Test: mail_message: partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids])
test_pids = set([p_a_id, p_b_id, p_c_id])
@@ -295,14 +330,16 @@ class test_mail(common.TransactionCase):
notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
# Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer)
- self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
+ for sent_email in sent_emails:
+ self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
# CASE2: post an email with attachments, parent_id, partner_ids
# TESTS: automatic subject, signature in body_html, attachments propagation
+ self._init_mock_build_email()
msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email',
partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments)
message = self.mail_message.browse(cr, uid, msg_id2)
- sent_email = self._build_email_kwargs
+ sent_emails = self._build_email_kwargs_list
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id2)]), 'mail.mail notifications should have been auto-deleted!')
# Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id
@@ -310,9 +347,11 @@ class test_mail(common.TransactionCase):
self.assertEqual(message.body, html_sanitize(_body2), 'mail.message body incorrect')
self.assertEqual(message.parent_id.id, msg_id, 'mail.message parent_id incorrect')
# Test: sent_email: email send by server: correct subject, body, body_alternative
- self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
- self.assertEqual(sent_email['body'], _mail_body2, 'sent_email body incorrect')
- self.assertEqual(sent_email['body_alternative'], _mail_bodyalt2, 'sent_email body_alternative incorrect')
+ self.assertEqual(len(sent_emails), 2, 'sent_email number of sent emails incorrect')
+ for sent_email in sent_emails:
+ self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
+ self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect')
+ self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect')
# Test: mail_message: partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids])
test_pids = set([p_a_id, p_b_id, p_c_id, p_d_id])
@@ -322,7 +361,8 @@ class test_mail(common.TransactionCase):
notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
# Test: sent_email: email_to should contain b@b, c@c, not a@a (writer)
- self.assertEqual(set(sent_email['email_to']), set(['b@b', 'c@c']), 'sent_email email_to incorrect')
+ for sent_email in sent_emails:
+ self.assertTrue(set(sent_email['email_to']).issubset(set(['b@b', 'c@c'])), 'sent_email email_to incorrect')
# Test: attachments
for attach in message.attachment_ids:
self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
diff --git a/addons/mail/wizard/__init__.py b/addons/mail/wizard/__init__.py
index 88b9581f68e..39a2d843354 100644
--- a/addons/mail/wizard/__init__.py
+++ b/addons/mail/wizard/__init__.py
@@ -19,6 +19,7 @@
#
##############################################################################
+import invite
import mail_compose_message
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py
new file mode 100644
index 00000000000..50ecf9450da
--- /dev/null
+++ b/addons/mail/wizard/invite.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2012-Today OpenERP SA (<http://www.openerp.com>)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from osv import osv
+from osv import fields
+from tools.translate import _
+
+
+class invite_wizard(osv.osv_memory):
+ """ Wizard to invite partners and make them followers. """
+ _name = 'mail.wizard.invite'
+ _description = 'Invite wizard'
+
+ def default_get(self, cr, uid, fields, context=None):
+ result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
+ if 'message' in fields and result.get('res_model') and result.get('res_id'):
+ document_name = self.pool.get(result.get('res_model')).name_get(cr, uid, [result.get('res_id')], context=context)[0][1]
+ message = _('<div>You have been invited to follow %s.</div>' % document_name)
+ result['message'] = message
+ elif 'message' in fields:
+ result['message'] = _('<div>You have been invited to follow a new document.</div>')
+ return result
+
+ _columns = {
+ 'res_model': fields.char('Related Document Model', size=128,
+ required=True, select=1,
+ help='Model of the followed resource'),
+ 'res_id': fields.integer('Related Document ID', select=1,
+ help='Id of the followed resource'),
+ 'partner_ids': fields.many2many('res.partner', string='Partners'),
+ 'message': fields.html('Message'),
+ }
+
+ def onchange_partner_ids(self, cr, uid, ids, value, context=None):
+ """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The
+ basic purpose of this method is to check that destination partners
+ effectively have email addresses. Otherwise a warning is thrown.
+ """
+ res = {'value': {}}
+ if not value or not value[0] or not value[0][0] == 6:
+ return
+ res.update(self.pool.get('mail.message').check_partners_email(cr, uid, value[0][2], context=context))
+ return res
+
+ def add_followers(self, cr, uid, ids, context=None):
+ for wizard in self.browse(cr, uid, ids, context=context):
+ model_obj = self.pool.get(wizard.res_model)
+ document = model_obj.browse(cr, uid, wizard.res_id, context=context)
+
+ # filter partner_ids to get the new followers, to avoid sending email to already following partners
+ new_follower_ids = [p.id for p in wizard.partner_ids if p.id not in document.message_follower_ids]
+ model_obj.message_subscribe(cr, uid, [wizard.res_id], new_follower_ids, context=context)
+
+ # send an email
+ if wizard.message:
+ for follower_id in new_follower_ids:
+ mail_mail = self.pool.get('mail.mail')
+ mail_id = mail_mail.create(cr, uid, {
+ 'subject': 'Invitation to follow %s' % document.name_get()[0][1],
+ 'body_html': '%s' % wizard.message,
+ 'auto_delete': True,
+ }, context=context)
+ mail_mail.send(cr, uid, [mail_id], recipient_ids=[follower_id], context=context)
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/mail/wizard/invite_view.xml b/addons/mail/wizard/invite_view.xml
new file mode 100644
index 00000000000..b932f96dfe7
--- /dev/null
+++ b/addons/mail/wizard/invite_view.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+
+ <!-- wizard view -->
+ <record model="ir.ui.view" id="mail_wizard_invite_form">
+ <field name="name">Add Followers</field>
+ <field name="model">mail.wizard.invite</field>
+ <field name="arch" type="xml">
+ <form string="Add Followers" version="7.0">
+ <group>
+ <field name="res_model" invisible="1"/>
+ <field name="res_id" invisible="1"/>
+ <field name="partner_ids" widget="many2many_tags"
+ on_change="onchange_partner_ids(partner_ids)" />
+ <field name="message"/>
+ </group>
+ <footer>
+ <button string="Add Followers"
+ name="add_followers" type="object" class="oe_highlight" />
+ or
+ <button string="Cancel" class="oe_link" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ </data>
+</openerp>
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 03c5a7cfe8b..760d16dbef2 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -193,24 +193,6 @@ class mail_compose_message(osv.TransientModel):
"""
return {'value': {'content_subtype': value}}
- def _verify_partner_email(self, cr, uid, partner_ids, context=None):
- """ Verify that selected partner_ids have an email_address defined.
- Otherwise throw a warning. """
- partner_wo_email_lst = []
- for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
- if not partner.email:
- partner_wo_email_lst.append(partner)
- if not partner_wo_email_lst:
- return {}
- warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
- for partner in partner_wo_email_lst:
- warning_msg += '\n- %s' % (partner.name)
- return {'warning': {
- 'title': _('Partners email addresses not found'),
- 'message': warning_msg,
- }
- }
-
def onchange_partner_ids(self, cr, uid, ids, value, context=None):
""" The basic purpose of this method is to check that destination partners
effectively have email addresses. Otherwise a warning is thrown.
@@ -219,7 +201,7 @@ class mail_compose_message(osv.TransientModel):
res = {'value': {}}
if not value or not value[0] or not value[0][0] == 6:
return
- res.update(self._verify_partner_email(cr, uid, value[0][2], context=context))
+ res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
return res
def dummy(self, cr, uid, ids, context=None):
diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py
index 425a7e3d7f8..e6976dd319c 100644
--- a/addons/point_of_sale/controllers/main.py
+++ b/addons/point_of_sale/controllers/main.py
@@ -72,19 +72,19 @@ class PointOfSaleController(openerpweb.Controller):
return getattr(self, method)(request, **kwargs)
@openerpweb.jsonrequest
- def scan_item_success(self, request):
+ def scan_item_success(self, request, ean):
"""
A product has been scanned with success
"""
- print 'scan_item_success'
+ print 'scan_item_success: ' + str(ean)
return
@openerpweb.jsonrequest
- def scan_item_error_unrecognized(self, request):
+ def scan_item_error_unrecognized(self, request, ean):
"""
A product has been scanned without success
"""
- print 'scan_item_error_unrecognized'
+ print 'scan_item_error_unrecognized: ' + str(ean)
return
@openerpweb.jsonrequest
@@ -166,4 +166,9 @@ class PointOfSaleController(openerpweb.Controller):
print 'print_receipt' + str(receipt)
return
+ @openerpweb.jsonrequest
+ def print_pdf_invoice(self, request, pdfinvoice):
+ print 'print_pdf_invoice' + str(pdfinvoice)
+ return
+
diff --git a/addons/point_of_sale/point_of_sale_demo.xml b/addons/point_of_sale/point_of_sale_demo.xml
index 7d80dd493ad..faa3927a9c4 100644
--- a/addons/point_of_sale/point_of_sale_demo.xml
+++ b/addons/point_of_sale/point_of_sale_demo.xml
@@ -6,17 +6,17 @@
</record>
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4,ref('group_pos_user'))]"/>
- <field name="ean13">0410200000005</field>
+ <field name="ean13">0420100000005</field>
</record>
<record id="account.cash_journal" model="account.journal">
<field eval="True" name="journal_user"/>
</record>
<record id="base.user_root" model="res.users">
- <field name="ean13">0410300000004</field>
+ <field name="ean13">0410100000006</field>
<field name="groups_id" eval="[(4,ref('group_pos_manager'))]"/>
</record>
<record id="base.user_demo" model="res.users">
- <field name="ean13">0410400000003</field>
+ <field name="ean13">0420100000005</field>
<field name="groups_id" eval="[(4,ref('group_pos_manager'))]"/>
</record>
@@ -189,6 +189,7 @@
<record id="citron" model="product.product">
<field name="list_price">1.98</field>
<field name="name">Lemon</field>
+ <field name="ean13">2301000000006</field>
<field name="to_weight">True</field>
<field name="pos_categ_id" ref="autres_agrumes"/>
<field name="uom_id" ref="product.product_uom_kgm" />
diff --git a/addons/point_of_sale/static/src/css/keyboard.css b/addons/point_of_sale/static/src/css/keyboard.css
index 3a06970929f..d5ce468d7be 100644
--- a/addons/point_of_sale/static/src/css/keyboard.css
+++ b/addons/point_of_sale/static/src/css/keyboard.css
@@ -71,7 +71,7 @@
clear: left;
}
.point-of-sale .keyboard .lastitem {
- margin-right: 0;
+ margin-right: 0 !important;
}
/* ---- full sized keyboard ---- */
@@ -79,13 +79,13 @@
.point-of-sale .full_keyboard {
list-style: none;
font-size: 14px;
- width: 680px;
+ width: 685px;
height: 100%;
- margin-left: auto;
- margin-right: auto;
+ margin-left: auto !important;
+ margin-right: auto !important;
}
.point-of-sale .full_keyboard li{
- margin: 0 5px 5px 0;
+ margin: 0 5px 5px 0 !important;
width: 40px;
height: 40px;
line-height: 40px;
@@ -115,22 +115,22 @@
.point-of-sale .simple_keyboard {
list-style: none;
font-size: 16px;
- width: 545px;
+ width: 555px;
height: 220px;
- margin-left: auto;
- margin-right: auto;
+ margin-left: auto !important;
+ margin-right: auto !important;
}
.point-of-sale .simple_keyboard li{
- margin: 0 5px 5px 0;
+ margin: 0 5px 5px 0 !important;
width: 49px;
height: 49px;
line-height: 49px;
}
.point-of-sale .simple_keyboard .firstitem.row_asdf{
- margin-left:25px;
+ margin-left:25px !important;
}
.point-of-sale .simple_keyboard .firstitem.row_zxcv{
- margin-left:55px;
+ margin-left:55px !important;
}
.point-of-sale .simple_keyboard .delete{
width: 103px;
@@ -139,7 +139,7 @@
width: 103px;
}
.point-of-sale .simple_keyboard .space{
- width:268px;
+ width:273px;
}
.point-of-sale .simple_keyboard .numlock{
width:103px;
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 9ac7d4405d0..8213df7fc44 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -373,7 +373,7 @@
/* ********* The product list ********* */
.point-of-sale .product-list {
- padding:10px;
+ padding:10px !important;
}
.point-of-sale .product-list-scroller{
@@ -470,7 +470,7 @@
}
.point-of-sale .category-list{
- padding:10px;
+ padding:10px !important;
}
/* d) the category button */
@@ -479,7 +479,7 @@
vertical-align: top;
display: inline-block;
font-size: 11px;
- margin: 5px;
+ margin: 5px !important;
width: 120px;
height:120px;
background:#fff;
@@ -566,7 +566,7 @@
display: inline-block;
line-height: 100px;
font-size: 11px;
- margin: 5px;
+ margin: 5px !important;
width: 120px;
height:120px;
background:#fff;
@@ -809,6 +809,8 @@
}
.point-of-sale .scale-screen .product-picture img{
+ max-width: 178px;
+ max-height:178px;
vertical-align: middle;
cursor:pointer;
}
@@ -860,7 +862,7 @@
.point-of-sale .goodbye-message{
position: absolute;
left:50%;
- top:30%;
+ top:40%;
width:500px;
height:400px;
margin-left: -250px;
@@ -1100,6 +1102,79 @@
.point-of-sale .pos-actionbar .button.rightalign{
float:right;
}
+/* ********* The Debug Widget ********* */
+
+.point-of-sale .debug-widget{
+ z-index:100000;
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ width: 200px;
+ font-size: 10px;
+
+ background: rgba(0,0,0,0.82);
+ color: white;
+ text-shadow: none;
+ padding-bottom: 10px;
+ box-shadow: 0px 3px 20px rgba(0,0,0,0.3);
+ cursor:move;
+}
+.point-of-sale .debug-widget .toggle{
+ position: absolute;
+ font-size: 16px;
+ cursor:pointer;
+ top:0px;
+ right:0px;
+ padding:10px;
+ padding-right:15px;
+}
+.point-of-sale .debug-widget .content{
+ overflow: hidden;
+}
+.point-of-sale .debug-widget h1{
+ background:black;
+ padding-top: 10px;
+ padding-left: 10px;
+ margin-top:0;
+ margin-bottom:0;
+}
+.point-of-sale .debug-widget .category{
+ background: black;
+ padding-left: 10px;
+ margin: 0px;
+ font-weight: bold;
+ padding-top:3px;
+ padding-bottom:3px;
+}
+.point-of-sale .debug-widget .button{
+ padding: 5px;
+ padding-left: 15px;
+ display: block;
+ cursor:pointer;
+}
+.point-of-sale .debug-widget .button:hover{
+ background: rgba(96,21,177,0.45);
+}
+.point-of-sale .debug-widget input{
+ margin-left:10px;
+ margin-top:7px;
+}
+.point-of-sale .debug-widget .status{
+ padding: 5px;
+ padding-left: 15px;
+ display: block;
+ cursor:default;
+}
+.point-of-sale .debug-widget .status.on{
+ background-color: #6cd11d;
+}
+.point-of-sale .debug-widget .event{
+ padding: 5px;
+ padding-left: 15px;
+ display: block;
+ cursor:default;
+ background-color: #1E1E1E;
+}
/* ********* The PopupWidgets ********* */
diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js
index 77f7307b48f..c80c1958aae 100644
--- a/addons/point_of_sale/static/src/js/db.js
+++ b/addons/point_of_sale/static/src/js/db.js
@@ -21,6 +21,8 @@ function openerp_pos_db(instance, module){
this.category_childs = {};
this.category_parent = {};
this.category_search_string = {};
+ this.packagings_by_id = {};
+ this.packagings_by_product_id = {};
},
/* returns the category object from its id. If you pass a list of id as parameters, you get
* a list of category objects.
@@ -116,6 +118,10 @@ function openerp_pos_db(instance, module){
if(product.ean13){
str += '|' + product.ean13;
}
+ var packagings = this.packagings_by_product_id[product.id] || [];
+ for(var i = 0; i < packagings.length; i++){
+ str += '|' + packagings[i].ean;
+ }
return str + '\n';
},
add_products: function(products){
@@ -158,6 +164,16 @@ function openerp_pos_db(instance, module){
this.save('products',stored_products);
this.save('categories',stored_categories);
},
+ add_packagings: function(packagings){
+ for(var i = 0, len = packagings.length; i < len; i++){
+ var pack = packagings[i];
+ this.packagings_by_id[pack.id] = pack;
+ if(!this.packagings_by_product_id[pack.product_id[0]]){
+ this.packagings_by_product_id[pack.product_id[0]] = [];
+ }
+ this.packagings_by_product_id[pack.product_id[0]].push(pack);
+ }
+ },
/* removes all the data from the database. TODO : being able to selectively remove data */
clear: function(stores){
for(var i = 0, len = arguments.length; i < len; i++){
@@ -184,6 +200,12 @@ function openerp_pos_db(instance, module){
return products[i];
}
}
+ for(var p in this.packagings_by_id){
+ var pack = this.packagings_by_id[p];
+ if( pack.ean === ean13){
+ return products[pack.product_id[0]];
+ }
+ }
return undefined;
},
get_product_by_category: function(category_id){
@@ -223,9 +245,12 @@ function openerp_pos_db(instance, module){
},
remove_order: function(order_id){
var orders = this.load('orders',[]);
+ console.log('Remove order:',order_id);
+ console.log('Order count:',orders.length);
orders = _.filter(orders, function(order){
return order.id !== order_id;
});
+ console.log('Order count:',orders.length);
this.save('orders',orders);
},
get_orders: function(){
diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js
index e36070414b6..a26d8ca7154 100644
--- a/addons/point_of_sale/static/src/js/devices.js
+++ b/addons/point_of_sale/static/src/js/devices.js
@@ -1,26 +1,6 @@
function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
- var debug_devices = new (instance.web.Class.extend({
- active: false,
- payment_status: 'waiting_for_payment',
- weight: 0,
- activate: function(){
- this.active = true;
- },
- deactivate: function(){
- this.active = false;
- },
- set_weight: function(weight){ this.activate(); this.weight = weight; },
- accept_payment: function(){ this.activate(); this.payment_status = 'payment_accepted'; },
- reject_payment: function(){ this.activate(); this.payment_status = 'payment_rejected'; },
- delay_payment: function(){ this.activate(); this.payment_status = 'waiting_for_payment'; },
- }))();
-
- if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
- window.debug_devices = debug_devices;
- }
-
// this object interfaces with the local proxy to communicate to the various hardware devices
// connected to the Point of Sale. As the communication only goes from the POS to the proxy,
// methods are used both to signal an event, and to fetch information.
@@ -38,32 +18,42 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.connection = new instance.web.JsonRPC();
this.connection.setup(url);
+
+ this.bypass_proxy = false;
+ this.notifications = {};
},
message : function(name,params,success_callback, error_callback){
success_callback = success_callback || function(){};
error_callback = error_callback || function(){};
-
- if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
- console.log('PROXY:',name,params);
+ var callbacks = this.notifications[name] || [];
+ for(var i = 0; i < callbacks.length; i++){
+ callbacks[i](params);
}
- if(!(debug_devices && debug_devices.active)){
- this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback);
+ this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback);
+ },
+
+ // this allows the client to be notified when a proxy call is made. The notification
+ // callback will be executed with the same arguments as the proxy call
+ add_notification: function(name, callback){
+ if(!this.notifications[name]){
+ this.notifications[name] = [];
}
+ this.notifications[name].push(callback);
},
//a product has been scanned and recognized with success
// ean is a parsed ean object
scan_item_success: function(ean){
- this.message('scan_item_success',ean);
+ this.message('scan_item_success',{ean: ean});
},
// a product has been scanned but not recognized
// ean is a parsed ean object
scan_item_error_unrecognized: function(ean){
- this.message('scan_item_error_unrecognized',ean);
+ this.message('scan_item_error_unrecognized',{ean: ean});
},
//the client is asking for help
@@ -78,12 +68,12 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
//the client is starting to weight
weighting_start: function(){
- this.weight = 0;
- if(debug_devices){
- debug_devices.weigth = 0;
+ if(!this.weighting){
+ this.weight = 0;
+ this.weighting = true;
+ this.bypass_proxy = false;
+ this.message('weighting_start');
}
- this.weighting = true;
- this.message('weighting_start');
},
//returns the weight on the scale.
@@ -91,22 +81,29 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// and a weighting_end()
weighting_read_kg: function(){
var self = this;
- if(debug_devices && debug_devices.active){
- return debug_devices.weight;
+ if(this.bypass_proxy){
+ return this.weight;
}else{
this.message('weighting_read_kg',{},function(weight){
- if(self.weighting){
+ if(self.weighting && !self.bypass_proxy){
self.weight = weight;
}
});
- return self.weight;
+ return this.weight;
}
},
+ // sets a custom weight, ignoring the proxy returned value until the next weighting_end
+ debug_set_weight: function(kg){
+ this.bypass_proxy = true;
+ this.weight = kg;
+ },
+
// the client has finished weighting products
weighting_end: function(){
this.weight = 0;
this.weighting = false;
+ this.bypass_proxy = false;
this.message('weighting_end');
},
@@ -116,9 +113,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
payment_request: function(price, method, info){
this.paying = true;
this.payment_status = 'waiting_for_payment';
- if(debug_devices){
- debug_devices.payment_status = 'waiting_for_payment';
- }
this.message('payment_request',{'price':price,'method':method,'info':info});
},
@@ -127,18 +121,30 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// returns 'waiting_for_payment' | 'payment_accepted' | 'payment_rejected'
is_payment_accepted: function(){
var self = this;
- if(debug_devices.active){
- return debug_devices.payment_status;
+ if(this.bypass_proxy){
+ this.bypass_proxy = false;
+ return this.payment_status;
}else{
this.message('is_payment_accepted', {}, function(payment_status){
if(self.paying){
self.payment_status = payment_status;
}
});
- return self.payment_status;
+ return this.payment_status;
}
},
+
+ // override what the proxy says and accept the payment
+ debug_accept_payment: function(){
+ this.bypass_proxy = true;
+ this.payment_status = 'payment_accepted';
+ },
+ // override what the proxy says and reject the payment
+ debug_reject_payment: function(){
+ this.bypass_proxy = true;
+ this.payment_status = 'payment_rejected';
+ },
// the client cancels his payment
payment_canceled: function(){
this.paying = false;
@@ -212,6 +218,11 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
print_receipt: function(receipt){
this.message('print_receipt',{receipt: receipt});
},
+
+ // asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server )
+ print_pdf_invoice: function(pdfinvoice){
+ this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
+ },
});
// this module interfaces with the barcode reader. It assumes the barcode reader
@@ -237,6 +248,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.cashier_prefix_set = attributes.cashier_prefix_set || {'041':''};
this.client_prefix_set = attributes.client_prefix_set || {'042':''};
},
+
save_callbacks: function(){
var callbacks = {};
for(name in this.action_callback){
@@ -244,6 +256,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
}
this.action_callback_stack.push(callbacks);
},
+
restore_callbacks: function(){
if(this.action_callback_stack.length){
var callbacks = this.action_callback_stack.pop();
@@ -337,7 +350,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
value: 0,
unit: 'none',
};
- console.log('ean',ean);
function match_prefix(prefix_set, type){
for(prefix in prefix_set){
@@ -380,6 +392,23 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
return parse_result;
},
+ on_ean: function(ean){
+ var parse_result = this.parse_ean(ean);
+
+ if (parse_result.type === 'error') { //most likely a checksum error, raise warning
+ console.warn('WARNING: barcode checksum error:',parse_result);
+ }else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
+ if(this.action_callback['product']){
+ this.action_callback['product'](parse_result);
+ }
+ //this.trigger("codebar",parse_result );
+ }else{
+ if(this.action_callback[parse_result.type]){
+ this.action_callback[parse_result.type](parse_result);
+ }
+ }
+ },
+
// starts catching keyboard events and tries to interpret codebar
// calling the callbacks when needed.
connect: function(){
@@ -410,21 +439,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
lastTimeStamp = new Date().getTime();
if (codeNumbers.length == 13) {
//We have found what seems to be a valid codebar
- var parse_result = self.parse_ean(codeNumbers.join(''));
-
- if (parse_result.type === 'error') { //most likely a checksum error, raise warning
- console.warn('WARNING: barcode checksum error:',parse_result);
- }else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
- if(self.action_callback['product']){
- self.action_callback['product'](parse_result);
- }
- //this.trigger("codebar",parse_result );
- }else{
- if(self.action_callback[parse_result.type]){
- self.action_callback[parse_result.type](parse_result);
- }
- }
-
+ self.on_ean(codeNumbers.join(''));
codeNumbers = [];
}
} else {
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 93860697492..2c6f4e8b6e0 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1,9 +1,6 @@
function openerp_pos_models(instance, module){ //module is instance.point_of_sale
var QWeb = instance.web.qweb;
- var fetch = function(model, fields, domain, ctx){
- return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
- };
// The PosModel contains the Point Of Sale's representation of the backend.
// Since the PoS must work in standalone ( Without connection to the server )
@@ -27,6 +24,8 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
this.db = new module.PosLS(); // a database used to store the products and categories
this.db.clear('products','categories');
+ this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
+
// default attributes values. If null, it will be loaded below.
this.set({
@@ -51,7 +50,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
'units': null,
'units_by_id': null,
- 'selectedOrder': undefined,
+ 'selectedOrder': null,
});
this.get('orders').bind('remove', function(){ self.on_removed_order(); });
@@ -70,15 +69,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
});
},
+ // helper function to load data from the server
+ fetch: function(model, fields, domain, ctx){
+ return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
+ },
// loads all the needed data on the sever. returns a deferred indicating when all the data has loaded.
load_server_data: function(){
var self = this;
- var loaded = fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
+ var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
.pipe(function(users){
self.set('user',users[0]);
- return fetch('res.company',
+ return self.fetch('res.company',
[
'currency_id',
'email',
@@ -93,15 +96,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(companies){
self.set('company',companies[0]);
- return fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
+ return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
}).pipe(function(company_partners){
self.get('company').contact_address = company_partners[0].contact_address;
- return fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]);
+ return self.fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]);
}).pipe(function(currencies){
self.set('currency',currencies[0]);
- return fetch('product.uom', null, null);
+ return self.fetch('product.uom', null, null);
}).pipe(function(units){
self.set('units',units);
var units_by_id = {};
@@ -110,19 +113,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}
self.set('units_by_id',units_by_id);
- return fetch('product.packaging', null, null);
+ return self.fetch('product.packaging', null, null);
}).pipe(function(packagings){
self.set('product.packaging',packagings);
- return fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
+ return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
}).pipe(function(users){
self.set('user_list',users);
- return fetch('account.tax', ['amount', 'price_include', 'type']);
+ return self.fetch('account.tax', ['amount', 'price_include', 'type']);
}).pipe(function(taxes){
self.set('taxes', taxes);
- return fetch(
+ return self.fetch(
'pos.session',
['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
[['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
@@ -130,7 +133,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(sessions){
self.set('pos_session', sessions[0]);
- return fetch(
+ return self.fetch(
'pos.config',
['name','journal_ids','shop_id','journal_id',
'iface_self_checkout', 'iface_led', 'iface_cashdrawer',
@@ -147,15 +150,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
self.iface_self_checkout = !!pos_config.iface_self_checkout;
self.iface_cashdrawer = !!pos_config.iface_cashdrawer;
- return fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]);
+ return self.fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]);
}).pipe(function(shops){
self.set('shop',shops[0]);
- return fetch('pos.category', ['id','name','parent_id','child_id','image'])
+ return self.fetch('product.packaging',['ean','product_id']);
+ }).pipe(function(packagings){
+ self.db.add_packagings(packagings);
+
+ return self.fetch('pos.category', ['id','name','parent_id','child_id','image'])
}).pipe(function(categories){
self.db.add_categories(categories);
- return fetch(
+ return self.fetch(
'product.product',
['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
@@ -165,7 +172,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(products){
self.db.add_products(products);
- return fetch(
+ return self.fetch(
'account.bank.statement',
['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
[['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]]
@@ -173,7 +180,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(bank_statements){
self.set('bank_statements', bank_statements);
- return fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]);
+ return self.fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]);
}).pipe(function(journals){
self.set('journals',journals);
@@ -226,6 +233,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order
push_order: function(record) {
+ console.log('PUSHING NEW ORDER:',record);
this.db.add_order(record);
this.flush();
},
@@ -241,10 +249,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// and remove the successfully sent ones from the db once
// it has been confirmed that they have been sent correctly.
flush: function() {
+ //TODO make the mutex work
+ console.log('FLUSH');
//this makes sure only one _int_flush is called at the same time
+ /*
return this.flush_mutex.exec(_.bind(function() {
return this._flush(0);
}, this));
+ */
+ this._flush(0);
},
// attempts to send an order of index 'index' in the list of order to send. The index
// is used to skip orders that failed. do not call this method outside the mutex provided
@@ -253,6 +266,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var self = this;
var orders = this.db.get_orders();
self.set('nbr_pending_operations',orders.length);
+ console.log('TRYING TO FLUSH ORDER:',index,'Of',orders.length);
var order = orders[index];
if(!order){
@@ -268,6 +282,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
})
.done(function(){
//remove from db if success
+ console.log('Order successfully sent');
self.db.remove_order(order.id);
self._flush(index);
});
@@ -301,6 +316,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
});
module.Product = Backbone.Model.extend({
+ get_image_url: function(){
+ return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+this.get('id');
+ },
});
module.ProductCollection = Backbone.Collection.extend({
@@ -330,7 +348,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_discount: function(){
return this.discount;
},
- // FIXME
get_product_type: function(){
return this.type;
},
@@ -546,6 +563,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.pos = attributes.pos;
this.selected_orderline = undefined;
this.screen_data = {}; // see ScreenSelector
+ this.receipt_type = 'receipt'; // 'receipt' || 'invoice'
return this;
},
generateUniqueId: function() {
@@ -617,6 +635,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
getDueLeft: function() {
return this.getTotal() - this.getPaidTotal();
},
+ // sets the type of receipt 'receipt'(default) or 'invoice'
+ set_receipt_type: function(type){
+ this.receipt_type = type;
+ },
+ get_receipt_type: function(){
+ return this.receipt_type;
+ },
// the client related to the current order.
set_client: function(client){
this.set('client',client);
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 9a2071c6335..d2c79f19928 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -65,6 +65,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
close_popup: function(){
if(this.current_popup){
+ this.current_popup.close();
this.current_popup.hide();
this.current_popup = null;
}
@@ -188,7 +189,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
return true;
}
}
- this.pos.proxy.scan_item_unrecognized(ean);
+ this.pos.proxy.scan_item_error_unrecognized(ean);
return false;
},
@@ -206,7 +207,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
return true;
}
}
- this.pos.proxy.scan_item_unrecognized(ean);
+ this.pos.proxy.scan_item_error_unrecognized(ean);
return false;
//TODO start the transaction
},
@@ -336,6 +337,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$el.show();
}
},
+ /* called before hide, when a popup is closed */
+ close: function(){
+ },
+ /* hides the popup. keep in mind that this is called in the initialization pass of the
+ * pos instantiation, so you don't want to do anything fancy in here */
hide: function(){
if(this.$el){
this.$el.hide();
@@ -352,9 +358,40 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$el.find('.button').off('click').click(function(){
self.pos_widget.screen_selector.close_popup();
- self.pos.proxy.help_canceled();
});
},
+ close:function(){
+ this.pos.proxy.help_canceled();
+ },
+ });
+
+ module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
+ template:'ChooseReceiptPopupWidget',
+ show: function(){
+ console.log('show');
+ this._super();
+ this.renderElement();
+ var self = this;
+ var currentOrder = self.pos.get('selectedOrder');
+
+ this.$('.button.receipt').off('click').click(function(){
+ currentOrder.set_receipt_type('receipt');
+ self.pos_widget.screen_selector.set_current_screen('products');
+ });
+
+ this.$('.button.invoice').off('click').click(function(){
+ currentOrder.set_receipt_type('invoice');
+ self.pos_widget.screen_selector.set_current_screen('products');
+ });
+ },
+ get_client_name: function(){
+ var client = this.pos.get('selectedOrder').get_client();
+ if( client ){
+ return client.name;
+ }else{
+ return '';
+ }
+ },
});
module.ErrorPopupWidget = module.PopUpWidget.extend({
@@ -492,10 +529,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
var product = this.get_product();
return (product ? product.get('list_price') : 0) || 0;
},
- get_product_image: function(){
- var product = this.get_product();
- return product ? product.get('image') : undefined;
- },
get_product_weight: function(){
return this.weight || 0;
},
@@ -527,23 +560,21 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
//we get the first cashregister marked as self-checkout
var selfCheckoutRegisters = [];
- for(var i = 0; i < this.pos.get('cashRegisters').models.length; i++){
- var cashregister = this.pos.get('cashRegisters').models[i];
+ for(var i = 0; i < self.pos.get('cashRegisters').models.length; i++){
+ var cashregister = self.pos.get('cashRegisters').models[i];
if(cashregister.self_checkout_payment_method){
selfCheckoutRegisters.push(cashregister);
}
}
- var cashregister = selfCheckoutRegisters[0] || this.pos.get('cashRegisters').models[0];
+ var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0];
currentOrder.addPaymentLine(cashregister);
-
- self.pos.push_order(currentOrder.exportAsJSON()).then(function() {
- currentOrder.destroy();
- self.pos.proxy.transaction_end();
- self.pos_widget.screen_selector.set_current_screen(self.next_screen);
- });
+ self.pos.push_order(currentOrder.exportAsJSON())
+ currentOrder.destroy();
+ self.pos.proxy.transaction_end();
+ self.pos_widget.screen_selector.set_current_screen(self.next_screen);
}else if(payment === 'payment_rejected'){
- clearInterval(this.intervalID);
+ clearInterval(self.intervalID);
//TODO show a tryagain thingie ?
}
},500);
@@ -571,19 +602,35 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show_numpad: false,
show_leftpane: false,
+ barcode_product_action: function(ean){
+ this.pos.proxy.transaction_start();
+ this._super(ean);
+ },
barcode_client_action: function(ean){
+ this.pos.proxy.transaction_start();
this._super(ean);
- this.pos_widget.screen_selector.set_current_screen(this.next_screen);
+ $('.goodbye-message').hide();
+ this.pos_widget.screen_selector.show_popup('choose-receipt');
},
show: function(){
this._super();
var self = this;
+
+ this.add_action_button({
+ label: 'help',
+ icon: '/point_of_sale/static/src/img/icons/png48/help.png',
+ click: function(){
+ $('.goodbye-message').css({opacity:1}).hide();
+ self.help_button_action();
+ },
+ });
+
$('.goodbye-message').css({opacity:1}).show();
setTimeout(function(){
$('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();});
- },3000);
+ },5000);
},
});
@@ -743,6 +790,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
this.updatePaymentSummary();
+ this.$('.paymentline-amout input').last().focus();
},
close: function(){
this._super();
@@ -830,7 +878,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.numpadState.set({mode: 'payment'});
},
set_value: function(val) {
- this.currentPaymentLines.last().set({amount: val});
+ this.currentPaymentLines.last().set_amount(val);
},
});
diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js
index 6de31c88dfd..f4d16b527c8 100644
--- a/addons/point_of_sale/static/src/js/widgets.js
+++ b/addons/point_of_sale/static/src/js/widgets.js
@@ -284,12 +284,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.click_product_action = options.click_product_action;
},
// returns the url of the product thumbnail
- get_image_url: function() {
- return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+this.model.get('id');
- },
renderElement: function() {
this._super();
- this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.get_image_url()));
+ this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.model.get_image_url()));
var self = this;
$("a", this.$el).click(function(e){
if(self.click_product_action){
@@ -667,9 +664,95 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
show: function(){ this.$el.show(); },
hide: function(){ this.$el.hide(); },
-
});
+ // The debug widget lets the user control and monitor the hardware and software status
+ // without the use of the proxy
+ module.DebugWidget = module.PosBaseWidget.extend({
+ template: "DebugWidget",
+ eans:{
+ admin_badge: '0410100000006',
+ client_badge: '0420100000005',
+ invalid_ean: '1232456',
+ soda_33cl: '5449000000996',
+ oranges_kg: '2100002031410',
+ lemon_price: '2301000001560',
+ unknown_product: '9900000000004',
+ },
+ events:[
+ 'scan_item_success',
+ 'scan_item_error_unrecognized',
+ 'payment_request',
+ 'open_cashbox',
+ 'print_receipt',
+ 'print_pdf_invoice',
+ 'weighting_read_kg',
+ 'is_payment_accepted',
+ ],
+ minimized: false,
+ start: function(){
+ var self = this;
+
+ this.$el.draggable();
+ this.$('.toggle').click(function(){
+ var content = self.$('.content');
+ var bg = self.$el;
+ if(!self.minimized){
+ content.animate({'height':'0'},200);
+ }else{
+ content.css({'height':'auto'});
+ }
+ self.minimized = !self.minimized;
+ });
+ this.$('.button.accept_payment').click(function(){
+ self.pos.proxy.debug_accept_payment();
+ });
+ this.$('.button.reject_payment').click(function(){
+ self.pos.proxy.debug_reject_payment();
+ });
+ this.$('.button.set_weight').click(function(){
+ var kg = Number(self.$('input.weight').val());
+ if(!Number.isNaN(kg)){
+ self.pos.proxy.debug_set_weight(kg);
+ }
+ });
+ this.$('.button.custom_ean').click(function(){
+ var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
+ self.$('input.ean').val(ean);
+ self.pos.barcode_reader.on_ean(ean);
+ });
+ _.each(this.eans, function(ean, name){
+ self.$('.button.'+name).click(function(){
+ self.$('input.ean').val(ean);
+ self.pos.barcode_reader.on_ean(ean);
+ });
+ });
+ _.each(this.events, function(name){
+ self.pos.proxy.add_notification(name,function(){
+ self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
+ self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
+ });
+ });
+ self.pos.proxy.add_notification('help_needed',function(){
+ self.$('.status.help_needed').addClass('on');
+ });
+ self.pos.proxy.add_notification('help_canceled',function(){
+ self.$('.status.help_needed').removeClass('on');
+ });
+ self.pos.proxy.add_notification('transaction_start',function(){
+ self.$('.status.transaction').addClass('on');
+ });
+ self.pos.proxy.add_notification('transaction_end',function(){
+ self.$('.status.transaction').removeClass('on');
+ });
+ self.pos.proxy.add_notification('weighting_start',function(){
+ self.$('.status.weighting').addClass('on');
+ });
+ self.pos.proxy.add_notification('weighting_end',function(){
+ self.$('.status.weighting').removeClass('on');
+ });
+ },
+ });
// ---------- Main Point of Sale Widget ----------
@@ -698,6 +781,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
+
// The PosWidget is the main widget that contains all other widgets in the PointOfSale.
// It is mainly composed of :
// - a header, containing the list of orders
@@ -722,14 +806,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.leftpane_width = '440px';
this.cashier_controls_visible = true;
this.image_cache = new module.ImageCache(); // for faster products image display
-
- /*
- //Epileptic mode
- setInterval(function(){
- $('body').css({'-webkit-filter':'hue-rotate('+Math.random()*360+'deg)' });
- },100);
- */
-
},
start: function() {
@@ -758,6 +834,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.screen_selector.set_default_screen();
+ window.screen_selector = self.screen_selector;
+
self.pos.barcode_reader.connect();
instance.webclient.set_content_full_screen(true);
@@ -771,11 +849,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();});
self.$('.loader img').hide();
- if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
- window.pos = self.pos;
- window.pos_widget = self.pos_widget;
- }
-
},function(){ // error when loading models data from the backend
self.$('.loader img').hide();
return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
@@ -831,6 +904,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.error_session_popup = new module.ErrorNoSessionPopupWidget(this, {});
this.error_session_popup.appendTo($('.point-of-sale'));
+ this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
+ this.choose_receipt_popup.appendTo($('.point-of-sale'));
+
// -------- Misc ---------
this.notification = new module.SynchNotificationWidget(this,{});
@@ -890,12 +966,17 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
'error': this.error_popup,
'error-product': this.error_product_popup,
'error-session': this.error_session_popup,
+ 'choose-receipt': this.choose_receipt_popup,
},
default_client_screen: 'welcome',
default_cashier_screen: 'products',
default_mode: this.pos.iface_self_checkout ? 'client' : 'cashier',
});
+ if(this.pos.debug){
+ this.debug_widget = new module.DebugWidget(this);
+ this.debug_widget.appendTo(this.$('#content'));
+ }
},
changed_pending_operations: function () {
@@ -965,9 +1046,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
try_close: function() {
var self = this;
- self.pos.flush().then(function() {
- self.close();
- });
+ //TODO : do the close after the flush...
+ self.pos.flush()
+ self.close();
},
close: function() {
var self = this;
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index bbe278758d5..7af3c2c2255 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -185,8 +185,8 @@
<span class="product-price">
<t t-esc="widget.format_currency(widget.get_product_price()) + '/Kg'" />
</span>
- <t t-if="widget.get_product_image()">
- <img t-att-src="'data:image/png;base64,'+ widget.get_product_image()" />
+ <t t-if="widget.get_product()">
+ <img t-att-src="widget.get_product().get_image_url()" />
</t>
</div>
</div>
@@ -313,18 +313,16 @@
</div>
</t>
- <t t-name="ReceiptPopupWidget">
+ <t t-name="ChooseReceiptPopupWidget">
<div class="modal-dialog">
<div class="popup popup-help">
- <p class="message">Welcome Mr. John Smith <br /> Choose your type of receipt:</p>
+ <p class="message">Welcome <t t-esc="widget.get_client_name()" /><br /> Choose your type of receipt:</p>
<div class = "button big-left receipt">
Ticket
</div>
<div class = "button big-right invoice">
Invoice
</div>
- <div class="footer">
- </div>
</div>
</div>
</t>
@@ -348,7 +346,8 @@
<t t-name="ErrorPopupWidget">
<div class="modal-dialog">
<div class="popup popup-help">
- <p class="message"><t t-esc=" widget.message || 'Error.' " /></p>
+ <p class="message"><t t-esc=" widget.message || 'Error' " /></p>
+ <p class="comment"><t t-esc=" widget.comment || '' "/></p>
</div>
</div>
</t>
@@ -415,6 +414,57 @@
</div>
</t>
+ <t t-name="DebugWidget">
+ <div class="debug-widget">
+ <h1>Debug Window</h1>
+ <div class="toggle">▾</div>
+ <div class="content">
+ <p class="category">Payment Terminal</p>
+
+ <ul>
+ <li class="button accept_payment">Accept Payment</li>
+ <li class="button reject_payment">Reject Payment</li>
+ </ul>
+ <p class="category">Electronic Scale</p>
+ <ul>
+ <li><input type="text" class="weight"></input></li>
+ <li class="button set_weight">Set Weight</li>
+ </ul>
+
+ <p class="category">Barcode Scanner</p>
+ <ul>
+ <li><input type="text" class="ean"></input></li>
+ <li class="button custom_ean">Custom Ean13</li>
+ <li class="button admin_badge">Admin Badge</li>
+ <li class="button client_badge">Client Badge</li>
+ <li class="button soda_33cl">Soda 33cl</li>
+ <li class="button oranges_kg">3.141Kg Oranges</li>
+ <li class="button lemon_price">1.54€ Lemon</li>
+ <li class="button unknown_product">Unknown Product</li>
+ <li class="button invalid_ean">Invalid Ean</li>
+ </ul>
+
+ <p class="category">Hardware Status</p>
+ <ul>
+ <li class="status help_needed">Help needed</li>
+ <li class="status weighting">Weighting</li>
+ <li class="status transaction">In Transaction</li>
+ </ul>
+ <p class="category">Hardware Events</p>
+ <ul>
+ <li class="event scan_item_success">Scan Item Success</li>
+ <li class="event scan_item_error_unrecognized">Scan Item Unrecognized</li>
+ <li class="event payment_request">Payment Request</li>
+ <li class="event open_cashbox">Open Cashbox</li>
+ <li class="event print_receipt">Print Receipt</li>
+ <li class="event print_pdf_invoice">Print Invoice</li>
+ <li class="event weighting_read_kg">Read Weighting Scale</li>
+ <li class="event is_payment_accepted">Check Payment</li>
+ </ul>
+ </div>
+ </div>
+ </t>
+
<t t-name="OrderlineWidget">
<li class="orderline">
<span class="product-name">
diff --git a/addons/portal/__init__.py b/addons/portal/__init__.py
index 9c6cc59be7d..2bfc6df0d96 100644
--- a/addons/portal/__init__.py
+++ b/addons/portal/__init__.py
@@ -20,6 +20,7 @@
##############################################################################
import portal
+import mail_mail
import wizard
diff --git a/addons/portal/mail_mail.py b/addons/portal/mail_mail.py
new file mode 100644
index 00000000000..72c8469c042
--- /dev/null
+++ b/addons/portal/mail_mail.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+import tools
+from osv import osv
+
+
+class mail_mail_portal(osv.Model):
+ """ Update of mail_mail class, to add the signin URL to notifications.
+ """
+ _name = 'mail.mail'
+ _inherit = ['mail.mail']
+
+ def _generate_signin_url(self, cr, uid, partner_id, portal_group_id, key, context=None):
+ """ Generate the signin url """
+ base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context)
+ return base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (partner_id, portal_group_id, key)
+
+ def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
+ """ Return a specific ir_email body. The main purpose of this method
+ is to be inherited by Portal, to add a link for signing in, in
+ each notification email a partner receives.
+
+ :param mail: mail.mail browse_record
+ :param partner: browse_record of the specific recipient partner
+ """
+ if partner:
+ portal_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal', 'portal')
+ portal_id = portal_ref and portal_ref[1] or False
+ url = self._generate_signin_url(cr, uid, partner.id, portal_id, 1234, context=context)
+ body = tools.append_content_to_html(mail.body_html, url)
+ return body
+ else:
+ return super(mail_mail_portal, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)
diff --git a/addons/portal/tests/__init__.py b/addons/portal/tests/__init__.py
new file mode 100644
index 00000000000..c4717f80ebf
--- /dev/null
+++ b/addons/portal/tests/__init__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from . import test_portal
+
+checks = [
+ test_portal,
+]
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file
diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py
new file mode 100644
index 00000000000..2c301228859
--- /dev/null
+++ b/addons/portal/tests/test_portal.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.addons.mail.tests import test_mail
+from openerp.tools import append_content_to_html
+
+
+class test_portal(test_mail.TestMailMockups):
+
+ def setUp(self):
+ super(test_portal, self).setUp()
+ self.ir_model = self.registry('ir.model')
+ self.mail_group = self.registry('mail.group')
+ self.mail_mail = self.registry('mail.mail')
+ self.res_users = self.registry('res.users')
+ self.res_partner = self.registry('res.partner')
+
+ # create a 'pigs' group that will be used through the various tests
+ self.group_pigs_id = self.mail_group.create(self.cr, self.uid,
+ {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
+
+ def test_00_mail_invite(self):
+ cr, uid = self.cr, self.uid
+ user_admin = self.res_users.browse(cr, uid, uid)
+ self.mail_invite = self.registry('mail.wizard.invite')
+ base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='')
+ portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'portal')
+ portal_id = portal_ref and portal_ref[1] or False
+
+ # 0 - Admin
+ p_a_id = user_admin.partner_id.id
+ # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
+ p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
+
+ # ----------------------------------------
+ # CASE1: generated URL
+ # ----------------------------------------
+
+ url = self.mail_mail._generate_signin_url(cr, uid, p_b_id, portal_id, 1234)
+ self.assertEqual(url, base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (p_b_id, portal_id, 1234),
+ 'generated signin URL incorrect')
+
+ # ----------------------------------------
+ # CASE2: invite Bert
+ # ----------------------------------------
+
+ _sent_email_subject = 'Invitation to follow Pigs'
+ _sent_email_body = append_content_to_html('<div>You have been invited to follow Pigs.</div>', url)
+
+ # Do: create a mail_wizard_invite, validate it
+ self._init_mock_build_email()
+ mail_invite_id = self.mail_invite.create(cr, uid, {'partner_ids': [(4, p_b_id)]}, {'default_res_model': 'mail.group', 'default_res_id': self.group_pigs_id})
+ self.mail_invite.add_followers(cr, uid, [mail_invite_id])
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+
+ # Test: Pigs followers should contain Admin and Bert
+ follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
+ self.assertEqual(set(follower_ids), set([p_a_id, p_b_id]), 'Pigs followers after invite is incorrect')
+ # Test: sent email subject, body
+ self.assertEqual(len(self._build_email_kwargs_list), 1, 'sent email number incorrect, should be only for Bert')
+ for sent_email in self._build_email_kwargs_list:
+ self.assertEqual(sent_email.get('subject'), _sent_email_subject, 'sent email subject incorrect')
+ self.assertEqual(sent_email.get('body'), _sent_email_body, 'sent email body incorrect')
diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py
index a2c45bead62..ee3aa942b7b 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.py
+++ b/addons/sale/wizard/sale_make_invoice_advance.py
@@ -30,7 +30,7 @@ class sale_advance_payment_inv(osv.osv_memory):
'advance_payment_method':fields.selection(
[('all', 'Invoice the whole sale order'), ('percentage','Percentage'), ('fixed','Fixed price (deposit)'),
('lines', 'Some order lines')],
- 'Invoice Method', required=True,
+ 'What do you want to invoice?', required=True,
help="""Use All to create the final invoice.
Use Percentage to invoice a percentage of the total amount.
Use Fixed Price to invoice a specific amound in advance.
diff --git a/addons/web_linkedin/static/src/js/linkedin.js b/addons/web_linkedin/static/src/js/linkedin.js
index 8f6a2b096a5..3cdfbfff410 100644
--- a/addons/web_linkedin/static/src/js/linkedin.js
+++ b/addons/web_linkedin/static/src/js/linkedin.js
@@ -117,6 +117,8 @@ openerp.web_linkedin = function(instance) {
"count": 25,
}).result(function(result) {
children_def.resolve(result);
+ }).error(function() {
+ children_def.reject();
});
defs.push(children_def.pipe(function(result) {
result = _.reject(result.people.values || [], function(el) {
@@ -130,6 +132,8 @@ openerp.web_linkedin = function(instance) {
var p_to_change = _.toArray(arguments);
to_change.child_ids = p_to_change;
});
+ }, function() {
+ return $.when();
}));
/* TODO
to_change.linkedinUrl = _.str.sprintf("http://www.linkedin.com/company/%d", entity.id);