[MERGE]: Merge with lp:openobject-addons

bzr revid: mma@tinyerp.com-20120914050233-abnyaijtxlw9zsxy
This commit is contained in:
Mayur Maheshwari (OpenERP) 2012-09-14 10:32:33 +05:30
commit 54a3b527f2
39 changed files with 1062 additions and 336 deletions

View File

@ -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}}

View File

@ -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)):

View File

@ -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"/>

View File

@ -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')

View File

@ -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>

View File

@ -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
}

View File

@ -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 !'})

View File

@ -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',

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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,
}
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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'));
},
});
};

View File

@ -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>

View File

@ -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">

View File

@ -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')

View File

@ -19,6 +19,7 @@
#
##############################################################################
import invite
import mail_compose_message
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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'}

View File

@ -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>

View File

@ -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):

View File

@ -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

View File

@ -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" />

View File

@ -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;

View File

@ -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 ********* */

View File

@ -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(){

View File

@ -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 {

View File

@ -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);

View File

@ -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);
},
});

View File

@ -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;

View File

@ -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">

View File

@ -20,6 +20,7 @@
##############################################################################
import portal
import mail_mail
import wizard

View File

@ -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)

View File

@ -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:

View File

@ -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')

View File

@ -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.

View File

@ -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);