[MERGE]sync with trunk

bzr revid: sgo@tinyerp.com-20130423052419-igsnmrv6n0m3s1d9
This commit is contained in:
sgo@tinyerp.com 2013-04-23 10:54:19 +05:30
commit fdcbf42052
133 changed files with 17642 additions and 349 deletions

View File

@ -1001,8 +1001,7 @@ class account_period(osv.osv):
def find(self, cr, uid, dt=None, context=None):
if context is None: context = {}
if not dt:
dt = fields.date.context_today(self,cr,uid,context=context)
#CHECKME: shouldn't we check the state of the period?
dt = fields.date.context_today(self, cr, uid, context=context)
args = [('date_start', '<=' ,dt), ('date_stop', '>=', dt)]
if context.get('company_id', False):
args.append(('company_id', '=', context['company_id']))
@ -1010,7 +1009,7 @@ class account_period(osv.osv):
company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
args.append(('company_id', '=', company_id))
result = []
if context.get('account_period_prefer_normal'):
if context.get('account_period_prefer_normal', True):
# look for non-special periods first, and fallback to all if no result is found
result = self.search(cr, uid, args + [('special', '=', False)], context=context)
if not result:
@ -1214,7 +1213,7 @@ class account_move(osv.osv):
return res
def _get_period(self, cr, uid, context=None):
ctx = dict(context or {}, account_period_prefer_normal=True)
ctx = dict(context or {})
period_ids = self.pool.get('account.period').find(cr, uid, context=ctx)
return period_ids[0]
@ -1786,7 +1785,7 @@ class account_tax_code(osv.osv):
if context.get('period_id', False):
period_id = context['period_id']
else:
period_id = self.pool.get('account.period').find(cr, uid)
period_id = self.pool.get('account.period').find(cr, uid, context=context)
if not period_id:
return dict.fromkeys(ids, 0.0)
period_id = period_id[0]

View File

@ -61,7 +61,7 @@ class account_bank_statement(osv.osv):
return res
def _get_period(self, cr, uid, context=None):
periods = self.pool.get('account.period').find(cr, uid,context=context)
periods = self.pool.get('account.period').find(cr, uid, context=context)
if periods:
return periods[0]
return False

View File

@ -286,7 +286,10 @@ class account_invoice(osv.osv):
'payment_ids': fields.function(_compute_lines, relation='account.move.line', type="many2many", string='Payments'),
'move_name': fields.char('Journal Entry', size=64, readonly=True, states={'draft':[('readonly',False)]}),
'user_id': fields.many2one('res.users', 'Salesperson', readonly=True, track_visibility='onchange', states={'draft':[('readonly',False)]}),
'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]})
'fiscal_position': fields.many2one('account.fiscal.position', 'Fiscal Position', readonly=True, states={'draft':[('readonly',False)]}),
'commercial_partner_id': fields.related('partner_id', 'commercial_partner_id', string='Commercial Entity', type='many2one',
relation='res.partner', store=True, readonly=True,
help="The commercial entity that will be used on Journal Entries for this invoice")
}
_defaults = {
'type': _get_type,
@ -985,8 +988,7 @@ class account_invoice(osv.osv):
'narration':inv.comment
}
period_id = inv.period_id and inv.period_id.id or False
ctx.update(company_id=inv.company_id.id,
account_period_prefer_normal=True)
ctx.update(company_id=inv.company_id.id)
if not period_id:
period_ids = period_obj.find(cr, uid, inv.date_invoice, context=ctx)
period_id = period_ids and period_ids[0] or False
@ -1262,9 +1264,7 @@ class account_invoice(osv.osv):
ref = invoice.reference
else:
ref = self._convert_ref(cr, uid, invoice.number)
partner = invoice.partner_id
if partner.parent_id and not partner.is_company:
partner = partner.parent_id
partner = self.pool['res.partner']._find_accounting_partner(invoice.partner_id)
# Pay attention to the sign for both debit/credit AND amount_currency
l1 = {
'debit': direction * pay_amount>0 and direction * pay_amount,
@ -1734,15 +1734,11 @@ class res_partner(osv.osv):
'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
}
def _find_accounting_partner(self, part):
def _find_accounting_partner(self, partner):
'''
Find the partner for which the accounting entries will be created
'''
#if the chosen partner is not a company and has a parent company, use the parent for the journal entries
#because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
if part.parent_id and not part.is_company:
part = part.parent_id
return part
return partner.commercial_partner_id
def copy(self, cr, uid, id, default=None, context=None):
default = default or {}

View File

@ -117,6 +117,7 @@
<field name="arch" type="xml">
<tree colors="blue:state == 'draft';black:state in ('proforma','proforma2','open');gray:state == 'cancel'" string="Invoice">
<field name="partner_id" groups="base.group_user"/>
<field name="commercial_partner_id" invisible="1"/>
<field name="date_invoice"/>
<field name="number"/>
<field name="reference" invisible="1"/>
@ -320,7 +321,8 @@
<field string="Customer" name="partner_id"
on_change="onchange_partner_id(type,partner_id,date_invoice,payment_term, partner_bank_id,company_id)"
groups="base.group_user" context="{'search_default_customer':1, 'show_address': 1}"
options='{"always_reload": True}'/>
options='{"always_reload": True}'
domain="[('customer', '=', True)]"/>
<field name="fiscal_position" widget="selection" />
</group>
<group>
@ -447,19 +449,20 @@
<field name="model">account.invoice</field>
<field name="arch" type="xml">
<search string="Search Invoice">
<field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('supplier_invoice_number', 'ilike', self), ('partner_id', 'ilike', self)]"/>
<field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('supplier_invoice_number', 'ilike', self), ('partner_id', 'child_of', self)]"/>
<filter name="draft" string="Draft" domain="[('state','=','draft')]" help="Draft Invoices"/>
<filter name="proforma" string="Proforma" domain="[('state','=','proforma2')]" help="Proforma Invoices" groups="account.group_proforma_invoices"/>
<filter name="invoices" string="Invoices" domain="[('state','not in',['draft','cancel'])]" help="Proforma/Open/Paid Invoices"/>
<filter name="unpaid" string="Unpaid" domain="[('state','=','open')]" help="Unpaid Invoices"/>
<separator/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
<field name="user_id" string="Salesperson"/>
<field name="period_id" string="Period"/>
<separator/>
<filter domain="[('user_id','=',uid)]" help="My Invoices"/>
<group expand="0" string="Group By...">
<filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter name="partner_id" string="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter name="commercial_partner_id" string="Commercial Partner" domain="[]" context="{'group_by':'commercial_partner_id'}"/>
<filter string="Responsible" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Journal" icon="terp-folder-orange" domain="[]" context="{'group_by':'journal_id'}"/>
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
@ -622,8 +625,6 @@
</record>
<menuitem action="action_invoice_tree4" id="menu_action_invoice_tree4" parent="menu_finance_payables"/>
<act_window context="{'search_default_partner_id':[active_id], 'default_partner_id': active_id}" id="act_res_partner_2_account_invoice_opened" name="Invoices" res_model="account.invoice" src_model="res.partner"/>
<act_window
id="act_account_journal_2_account_invoice_opened"
name="Unpaid Invoices"

View File

@ -559,10 +559,11 @@ class account_move_line(osv.osv):
]
def _auto_init(self, cr, context=None):
super(account_move_line, self)._auto_init(cr, context=context)
res = super(account_move_line, self)._auto_init(cr, context=context)
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'account_move_line_journal_id_period_id_index\'')
if not cr.fetchone():
cr.execute('CREATE INDEX account_move_line_journal_id_period_id_index ON account_move_line (journal_id, period_id)')
return res
def _check_no_view(self, cr, uid, ids, context=None):
lines = self.browse(cr, uid, ids, context=context)
@ -654,13 +655,7 @@ class account_move_line(osv.osv):
}
return result
def onchange_account_id(self, cr, uid, ids, account_id, context=None):
res = {'value': {}}
if account_id:
res['value']['account_tax_id'] = [x.id for x in self.pool.get('account.account').browse(cr, uid, account_id, context=context).tax_ids]
return res
def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False):
def onchange_partner_id(self, cr, uid, ids, move_id, partner_id, account_id=None, debit=0, credit=0, date=False, journal=False, context=None):
partner_obj = self.pool.get('res.partner')
payment_term_obj = self.pool.get('account.payment.term')
journal_obj = self.pool.get('account.journal')
@ -674,8 +669,8 @@ class account_move_line(osv.osv):
date = datetime.now().strftime('%Y-%m-%d')
jt = False
if journal:
jt = journal_obj.browse(cr, uid, journal).type
part = partner_obj.browse(cr, uid, partner_id)
jt = journal_obj.browse(cr, uid, journal, context=context).type
part = partner_obj.browse(cr, uid, partner_id, context=context)
payment_term_id = False
if jt and jt in ('purchase', 'purchase_refund') and part.property_supplier_payment_term:
@ -700,20 +695,20 @@ class account_move_line(osv.osv):
elif part.supplier:
val['account_id'] = fiscal_pos_obj.map_account(cr, uid, part and part.property_account_position or False, id1)
if val.get('account_id', False):
d = self.onchange_account_id(cr, uid, ids, val['account_id'])
d = self.onchange_account_id(cr, uid, ids, account_id=val['account_id'], partner_id=part.id, context=context)
val.update(d['value'])
return {'value':val}
def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False):
def onchange_account_id(self, cr, uid, ids, account_id=False, partner_id=False, context=None):
account_obj = self.pool.get('account.account')
partner_obj = self.pool.get('res.partner')
fiscal_pos_obj = self.pool.get('account.fiscal.position')
val = {}
if account_id:
res = account_obj.browse(cr, uid, account_id)
res = account_obj.browse(cr, uid, account_id, context=context)
tax_ids = res.tax_ids
if tax_ids and partner_id:
part = partner_obj.browse(cr, uid, partner_id)
part = partner_obj.browse(cr, uid, partner_id, context=context)
tax_id = fiscal_pos_obj.map_tax(cr, uid, part and part.property_account_position or False, tax_ids)[0]
else:
tax_id = tax_ids and tax_ids[0].id or False
@ -985,8 +980,7 @@ class account_move_line(osv.osv):
if context is None:
context = {}
period_pool = self.pool.get('account.period')
ctx = dict(context, account_period_prefer_normal=True)
pids = period_pool.find(cr, user, date, context=ctx)
pids = period_pool.find(cr, user, date, context=context)
if pids:
res.update({
'period_id':pids[0]

View File

@ -1112,7 +1112,7 @@
<field name="ref"/>
<field name="statement_id" invisible="1"/>
<field name="partner_id" on_change="onchange_partner_id(move_id, partner_id, account_id, debit, credit, date, journal_id)"/>
<field name="account_id" options='{"no_open":True}' domain="[('journal_id','=',journal_id), ('company_id', '=', company_id)]" on_change="onchange_account_id(account_id)"/>
<field name="account_id" options='{"no_open":True}' domain="[('journal_id','=',journal_id), ('company_id', '=', company_id)]" on_change="onchange_account_id(account_id, partner_id, context)"/>
<field name="account_tax_id" options='{"no_open":True}' invisible="context.get('journal_type', False) not in ['sale','sale_refund','purchase','purchase_refund','general']"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" domain="[('type','not in',['view','template'])]" invisible="not context.get('analytic_journal_id',False)"/>
<field name="move_id" required="0"/>
@ -1194,7 +1194,12 @@
sequence="1"
groups="group_account_user"
/>
<record id="action_account_moves_all_tree" model="ir.actions.act_window">
<field name="name">Journal Items</field>
<field name="res_model">account.move.line</field>
<field name="context">{'search_default_partner_id': [active_id], 'default_partner_id': active_id}</field>
<field name="view_id" ref="view_move_line_tree"/>
</record>
<record id="view_move_line_tree_reconcile" model="ir.ui.view">
<field name="model">account.move.line</field>
<field eval="24" name="priority"/>
@ -1288,7 +1293,7 @@
<group col="6" colspan="4">
<field name="name"/>
<field name="ref"/>
<field name="partner_id" on_change="onchange_partner_id(False,partner_id,account_id,debit,credit,date)"/>
<field name="partner_id" on_change="onchange_partner_id(False, partner_id, account_id, debit, credit, date, journal_id, context)"/>
<field name="journal_id"/>
<field name="period_id"/>
@ -1352,7 +1357,7 @@
<tree colors="blue:state == 'draft';black:state == 'posted'" editable="top" string="Journal Items">
<field name="invoice"/>
<field name="name"/>
<field name="partner_id" on_change="onchange_partner_id(False,partner_id,account_id,debit,credit,parent.date,parent.journal_id)"/>
<field name="partner_id" on_change="onchange_partner_id(False, partner_id, account_id, debit, credit, parent.date, parent.journal_id, context)"/>
<field name="account_id" domain="[('journal_id','=',parent.journal_id),('company_id', '=', parent.company_id)]"/>
<field name="date_maturity"/>
<field name="debit" sum="Total Debit"/>
@ -1771,23 +1776,6 @@
</field>
</record>
<!-- res.partner links -->
<act_window
context="{'search_default_unreconciled':True, 'search_default_partner_id':[active_id], 'default_partner_id': active_id}"
domain="[('account_id.reconcile', '=', True),('account_id.type', 'in', ['receivable', 'payable'])]"
id="act_account_partner_account_move_all"
name="Receivables &amp; Payables"
res_model="account.move.line"
src_model="res.partner"/>
<act_window
context="{'search_default_partner_id':[active_id], 'default_partner_id': active_id}"
id="act_account_partner_account_move"
name="Journal Items"
res_model="account.move.line"
src_model="res.partner"
groups="account.group_account_user"/>
<!-- Account Templates -->
<menuitem
id="account_template_folder"

View File

@ -7,15 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2012-12-22 23:17+0000\n"
"Last-Translator: Fábio Martinelli - http://zupy.com.br "
"<webmaster@guaru.net>\n"
"PO-Revision-Date: 2013-04-18 17:44+0000\n"
"Last-Translator: Thiago Tognoli <Unknown>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-03-16 05:19+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"X-Launchpad-Export-Date: 2013-04-19 05:24+0000\n"
"X-Generator: Launchpad (build 16567)\n"
#. module: account
#: model:process.transition,name:account.process_transition_supplierreconcilepaid0
@ -431,7 +430,7 @@ msgstr "Data de criação"
#. module: account
#: selection:account.journal,type:0
msgid "Purchase Refund"
msgstr "Devolução da Venda"
msgstr "Devolução de Compra"
#. module: account
#: selection:account.journal,type:0
@ -12678,13 +12677,6 @@ msgstr ""
#~ msgid "This period is already closed !"
#~ msgstr "Este período já está fechado"
#, python-format
#~ msgid ""
#~ "Selected Move lines does not have any account move enties in draft state"
#~ msgstr ""
#~ "As linhas do movimento selecionado nao tem nenhuma conta a ser movida para o "
#~ "estado de esboço"
#~ msgid "Unpaid Customer Refunds"
#~ msgstr "Reembolsos a clientes não pagos"
@ -13232,6 +13224,13 @@ msgstr ""
#~ msgid "Can not %s draft/proforma/cancel invoice."
#~ msgstr "Não pode %s provisório/proforma/cancelar fatura."
#, python-format
#~ msgid ""
#~ "Selected Move lines does not have any account move enties in draft state"
#~ msgstr ""
#~ "As linhas de movimento selecionadas não tem nenhum movimento nesta conta no "
#~ "modo provisório"
#, python-format
#~ msgid "Can not pay draft/proforma/cancel invoice."
#~ msgstr "Não se pode pagar uma fatura provisória/proforma/cancelada"

View File

@ -23,10 +23,16 @@ import datetime
from dateutil.relativedelta import relativedelta
import logging
from operator import itemgetter
from os.path import join as opj
import time
import urllib2
import urlparse
from openerp import tools
try:
import simplejson as json
except ImportError:
import json # noqa
from openerp.release import serie
from openerp.tools.translate import _
from openerp.osv import fields, osv
@ -38,13 +44,28 @@ class account_installer(osv.osv_memory):
def _get_charts(self, cr, uid, context=None):
modules = self.pool.get('ir.module.module')
# try get the list on apps server
try:
apps_server = self.pool.get('ir.config_parameter').get_param(cr, uid, 'apps.server', 'https://apps.openerp.com')
up = urlparse.urlparse(apps_server)
url = '{0.scheme}://{0.netloc}/apps/charts?serie={1}'.format(up, serie)
j = urllib2.urlopen(url, timeout=3).read()
apps_charts = json.loads(j)
charts = dict(apps_charts)
except Exception:
charts = dict()
# Looking for the module with the 'Account Charts' category
category_name, category_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'module_category_localization_account_charts')
ids = modules.search(cr, uid, [('category_id', '=', category_id)], context=context)
charts = list(
sorted(((m.name, m.shortdesc)
for m in modules.browse(cr, uid, ids, context=context)),
key=itemgetter(1)))
if ids:
charts.update((m.name, m.shortdesc) for m in modules.browse(cr, uid, ids, context=context))
charts = sorted(charts.items(), key=itemgetter(1))
charts.insert(0, ('configurable', _('Custom')))
return charts
@ -57,9 +78,9 @@ class account_installer(osv.osv_memory):
"country."),
'date_start': fields.date('Start Date', required=True),
'date_stop': fields.date('End Date', required=True),
'period': fields.selection([('month', 'Monthly'), ('3months','3 Monthly')], 'Periods', required=True),
'period': fields.selection([('month', 'Monthly'), ('3months', '3 Monthly')], 'Periods', required=True),
'company_id': fields.many2one('res.company', 'Company', required=True),
'has_default_company' : fields.boolean('Has Default Company', readonly=True),
'has_default_company': fields.boolean('Has Default Company', readonly=True),
}
def _default_company(self, cr, uid, context=None):
@ -78,30 +99,29 @@ class account_installer(osv.osv_memory):
'has_default_company': _default_has_default_company,
'charts': 'configurable'
}
def get_unconfigured_cmp(self, cr, uid, context=None):
""" get the list of companies that have not been configured yet
but don't care about the demo chart of accounts """
cmp_select = []
company_ids = self.pool.get('res.company').search(cr, uid, [], context=context)
cr.execute("SELECT company_id FROM account_account WHERE active = 't' AND account_account.parent_id IS NULL AND name != %s", ("Chart For Automated Tests",))
configured_cmp = [r[0] for r in cr.fetchall()]
return list(set(company_ids)-set(configured_cmp))
def check_unconfigured_cmp(self, cr, uid, context=None):
""" check if there are still unconfigured companies """
if not self.get_unconfigured_cmp(cr, uid, context=context):
raise osv.except_osv(_('No unconfigured company !'), _("There is currently no company without chart of account. The wizard will therefore not be executed."))
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
if context is None:context = {}
res = super(account_installer, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar,submenu=False)
if context is None: context = {}
res = super(account_installer, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=False)
cmp_select = []
# display in the widget selection only the companies that haven't been configured yet
unconfigured_cmp = self.get_unconfigured_cmp(cr, uid, context=context)
for field in res['fields']:
if field == 'company_id':
res['fields'][field]['domain'] = [('id','in',unconfigured_cmp)]
res['fields'][field]['domain'] = [('id', 'in', unconfigured_cmp)]
res['fields'][field]['selection'] = [('', '')]
if unconfigured_cmp:
cmp_select = [(line.id, line.name) for line in self.pool.get('res.company').browse(cr, uid, unconfigured_cmp)]
@ -117,7 +137,7 @@ class account_installer(osv.osv_memory):
def execute(self, cr, uid, ids, context=None):
self.execute_simple(cr, uid, ids, context)
super(account_installer, self).execute(cr, uid, ids, context=context)
return super(account_installer, self).execute(cr, uid, ids, context=context)
def execute_simple(self, cr, uid, ids, context=None):
if context is None:
@ -129,8 +149,8 @@ class account_installer(osv.osv_memory):
if not f_ids:
name = code = res['date_start'][:4]
if int(name) != int(res['date_stop'][:4]):
name = res['date_start'][:4] +'-'+ res['date_stop'][:4]
code = res['date_start'][2:4] +'-'+ res['date_stop'][2:4]
name = res['date_start'][:4] + '-' + res['date_stop'][:4]
code = res['date_start'][2:4] + '-' + res['date_stop'][2:4]
vals = {
'name': name,
'code': code,
@ -150,7 +170,7 @@ class account_installer(osv.osv_memory):
chart = self.read(cr, uid, ids, ['charts'],
context=context)[0]['charts']
_logger.debug('Installing chart of accounts %s', chart)
return modules | set([chart])
return (modules | set([chart])) - set(['has_default_company', 'configurable'])
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -233,5 +233,10 @@ class res_partner(osv.osv):
'last_reconciliation_date': fields.datetime('Latest Full Reconciliation Date', help='Date on which the partner accounting entries were fully reconciled last time. It differs from the last date where a reconciliation has been made for this partner, as here we depict the fact that nothing more was to be reconciled at this date. This can be achieved in 2 different ways: either the last unreconciled debit/credit entry of this partner was reconciled, either the user pressed the button "Nothing more to reconcile" during the manual reconciliation process.')
}
def _commercial_fields(self, cr, uid, context=None):
return super(res_partner, self)._commercial_fields(cr, uid, context=context) + \
['debit_limit', 'property_account_payable', 'property_account_receivable', 'property_account_position',
'property_payment_term', 'property_supplier_payment_term', 'last_reconciliation_date']
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -50,6 +50,29 @@
</field>
</record>
<record id="action_account_analytic_account_form" model="ir.actions.act_window">
<field name="context">{'search_default_partner_id': [active_id], 'default_partner_id': active_id}</field>
<field name="name">Contracts/Analytic Accounts</field>
<field name="res_model">account.analytic.account</field>
<field name="view_mode">tree,form</field>
</record>
<record model="ir.ui.view" id="partner_view_buttons">
<field name="name">partner.view.buttons</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='buttons']" position="inside">
<button type="action" string="Invoices"
name="%(account.action_invoice_tree)d"
context="{'search_default_partner_id': active_id,'default_partner_id': active_id}" groups="account.group_account_invoice"/>
<button type="action" string="Journal Items" name="%(account.action_account_moves_all_tree)d" groups="account.group_account_user"/>
<button type="action" string="Contracts/Analytic Accounts" name="%(account.action_account_analytic_account_form)d"
groups="analytic.group_analytic_accounting"/>
</xpath>
</field>
</record>
<record id="action_account_fiscal_position_form" model="ir.actions.act_window">
<field name="name">Fiscal Positions</field>
<field name="res_model">account.fiscal.position</field>
@ -73,7 +96,7 @@
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<page string="History" position="before" version="7.0">
<page string="Accounting" col="4">
<page string="Accounting" col="4" name="accounting" attrs="{'invisible': [('is_company','=',False),('parent_id','!=',False)]}">
<group>
<group>
<field name="property_account_position" widget="selection"/>
@ -103,20 +126,13 @@
</tree>
</field>
</page>
<page string="Accounting" name="accounting_disabled" attrs="{'invisible': ['|',('is_company','=',True),('parent_id','=',False)]}">
<div>
<p>Accounting-related settings are managed on <button name="open_commercial_entity" type="object" string="the parent company" class="oe_link"/></p>
</div>
</page>
</page>
</field>
</record>
<!-- Partners info tab view-->
<act_window
id="action_analytic_open"
name="Contracts/Analytic Accounts"
res_model="account.analytic.account"
context="{'search_default_partner_id':[active_id], 'default_partner_id': active_id}"
src_model="res.partner"
view_type="form"
view_mode="tree,form"/>
</data>
</openerp>

View File

@ -31,7 +31,7 @@
<search string="Analytic Account">
<field name="name" filter_domain="['|', ('name','ilike',self), ('code','ilike',self)]" string="Analytic Account"/>
<field name="date"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="manager_id"/>
<field name="parent_id"/>
<field name="user_id"/>

View File

@ -81,7 +81,7 @@ class account_entries_report(osv.osv):
period_obj = self.pool.get('account.period')
for arg in args:
if arg[0] == 'period_id' and arg[2] == 'current_period':
current_period = period_obj.find(cr, uid)[0]
current_period = period_obj.find(cr, uid, context=context)[0]
args.append(['period_id','in',[current_period]])
break
elif arg[0] == 'period_id' and arg[2] == 'current_year':
@ -100,7 +100,7 @@ class account_entries_report(osv.osv):
fiscalyear_obj = self.pool.get('account.fiscalyear')
period_obj = self.pool.get('account.period')
if context.get('period', False) == 'current_period':
current_period = period_obj.find(cr, uid)[0]
current_period = period_obj.find(cr, uid, context=context)[0]
domain.append(['period_id','in',[current_period]])
elif context.get('year', False) == 'current_year':
current_year = fiscalyear_obj.find(cr, uid)

View File

@ -70,6 +70,7 @@ class account_invoice_report(osv.osv):
'categ_id': fields.many2one('product.category','Category of Product', readonly=True),
'journal_id': fields.many2one('account.journal', 'Journal', readonly=True),
'partner_id': fields.many2one('res.partner', 'Partner', readonly=True),
'commercial_partner_id': fields.many2one('res.partner', 'Partner Company', help="Commercial Entity"),
'company_id': fields.many2one('res.company', 'Company', readonly=True),
'user_id': fields.many2one('res.users', 'Salesperson', readonly=True),
'price_total': fields.float('Total Without Tax', readonly=True),
@ -108,7 +109,7 @@ class account_invoice_report(osv.osv):
sub.fiscal_position, sub.user_id, sub.company_id, sub.nbr, sub.type, sub.state,
sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id,
sub.product_qty, sub.price_total / cr.rate as price_total, sub.price_average /cr.rate as price_average,
cr.rate as currency_rate, sub.residual / cr.rate as residual
cr.rate as currency_rate, sub.residual / cr.rate as residual, sub.commercial_partner_id as commercial_partner_id
"""
return select_str
@ -170,7 +171,8 @@ class account_invoice_report(osv.osv):
LEFT JOIN account_invoice a ON a.id = l.invoice_id
WHERE a.id = ai.id)
ELSE 1::bigint
END::numeric AS residual
END::numeric AS residual,
ai.commercial_partner_id as commercial_partner_id
"""
return select_str
@ -193,7 +195,7 @@ class account_invoice_report(osv.osv):
ai.partner_id, ai.payment_term, ai.period_id, u.name, ai.currency_id, ai.journal_id,
ai.fiscal_position, ai.user_id, ai.company_id, ai.type, ai.state, pt.categ_id,
ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual,
ai.amount_total, u.uom_type, u.category_id
ai.amount_total, u.uom_type, u.category_id, ai.commercial_partner_id
"""
return group_by_str

View File

@ -14,6 +14,7 @@
<field name="type" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="commercial_partner_id" invisible="1"/>
<field name="product_id" invisible="1"/>
<field name="uom_name" invisible="not context.get('set_visible',False)"/>
<field name="categ_id" invisible="1"/>
@ -65,7 +66,8 @@
<field name="user_id" />
<field name="categ_id" filter_domain="[('categ_id', 'child_of', self)]"/>
<group expand="1" string="Group By...">
<filter string="Partner" name="partner" icon="terp-partner" context="{'group_by':'partner_id','residual_visible':True}"/>
<filter string="Partner" name="partner_id" context="{'group_by':'partner_id','residual_visible':True}"/>
<filter string="Commercial Partner" name="commercial_partner_id" context="{'group_by':'commercial_partner_id','residual_visible':True}"/>
<filter string="Salesperson" name='user' icon="terp-personal" context="{'group_by':'user_id'}"/>
<filter string="Due Date" icon="terp-go-today" context="{'group_by':'date_due'}"/>
<filter string="Period" icon="terp-go-month" context="{'group_by':'period_id'}" name="period"/>

View File

@ -26,7 +26,7 @@ openerp.account = function (instance) {
if (this.partners) {
this.$el.prepend(QWeb.render("AccountReconciliation", {widget: this}));
this.$(".oe_account_recon_previous").click(function() {
self.current_partner = (self.current_partner - 1) % self.partners.length;
self.current_partner = (((self.current_partner - 1) % self.partners.length) + self.partners.length) % self.partners.length;
self.search_by_partner();
});
this.$(".oe_account_recon_next").click(function() {

View File

@ -148,7 +148,6 @@ class account_move_line_reconcile_writeoff(osv.osv_memory):
context['analytic_id'] = data['analytic_id'][0]
if context['date_p']:
date = context['date_p']
ids = period_obj.find(cr, uid, dt=date, context=context)
if ids:
period_id = ids[0]

View File

@ -38,7 +38,7 @@ class account_tax_chart(osv.osv_memory):
def _get_period(self, cr, uid, context=None):
"""Return default period value"""
period_ids = self.pool.get('account.period').find(cr, uid)
period_ids = self.pool.get('account.period').find(cr, uid, context=context)
return period_ids and period_ids[0] or False
def account_tax_chart_open_window(self, cr, uid, ids, context=None):

View File

@ -213,7 +213,7 @@
<search string="Contracts">
<field name="name" filter_domain="['|', ('name','ilike',self),('code','ilike',self)]" string="Contract"/>
<field name="date"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="manager_id"/>
<field name="parent_id"/>
<filter name="open" string="In Progress" domain="[('state','in',('open','draft'))]" help="Contracts in progress (open, draft)"/>

View File

@ -82,7 +82,7 @@ class account_asset_asset(osv.osv):
return super(account_asset_asset, self).unlink(cr, uid, ids, context=context)
def _get_period(self, cr, uid, context=None):
periods = self.pool.get('account.period').find(cr, uid)
periods = self.pool.get('account.period').find(cr, uid, context=context)
if periods:
return periods[0]
else:

View File

@ -223,7 +223,7 @@
<filter icon="terp-check" string="Current" domain="[('state','in', ('draft','open'))]" help="Assets in draft and open states"/>
<filter icon="terp-dialog-close" string="Closed" domain="[('state','=', 'close')]" help="Assets in closed state"/>
<field name="category_id"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
</search>
</field>
</record>

View File

@ -49,7 +49,7 @@
<field name="asset_id"/>
<field name="asset_category_id"/>
<group expand="0" string="Extended Filters...">
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group expand="1" string="Group By...">

View File

@ -30,7 +30,7 @@ class asset_depreciation_confirmation_wizard(osv.osv_memory):
}
def _get_period(self, cr, uid, context=None):
periods = self.pool.get('account.period').find(cr, uid)
periods = self.pool.get('account.period').find(cr, uid, context=context)
if periods:
return periods[0]
return False

View File

@ -10,7 +10,7 @@
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<tree string="Customer Followup">
<field name="name"/>
<field name="display_name"/>
<field name="payment_next_action_date"/>
<field name="payment_next_action"/>
<field name="user_id" invisible="1"/>
@ -29,7 +29,7 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="display_name" position="after">
<field name="payment_responsible_id" invisible="1"/>
</field>
</field>

View File

@ -84,7 +84,7 @@ class account_voucher(osv.osv):
if context is None: context = {}
if context.get('period_id', False):
return context.get('period_id')
periods = self.pool.get('account.period').find(cr, uid)
periods = self.pool.get('account.period').find(cr, uid, context=context)
return periods and periods[0] or False
def _make_journal_search(self, cr, uid, ttype, context=None):

View File

@ -129,7 +129,7 @@
<filter icon="terp-camera_test" string="Posted" domain="[('state','=','posted')]" help="Posted Vouchers"/>
<separator/>
<filter icon="terp-gtk-jump-to-ltr" string="To Review" domain="[('state','=','posted'), ('audit','=',False)]" help="To Review"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
<field name="journal_id" context="{'journal_id': self, 'set_visible':False}" />
<field name="period_id"/>
<group expand="0" string="Group By...">

View File

@ -11,7 +11,7 @@
<field name="date"/>
<filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Draft Vouchers"/>
<filter icon="terp-camera_test" string="Posted" domain="[('state','=','posted')]" help="Posted Vouchers"/>
<field name="partner_id" string="Customer"/>
<field name="partner_id" string="Customer" filter_domain="[('partner_id','child_of',self)]"/>
<field name="journal_id" context="{'journal_id': self, 'set_visible':False}" domain="[('type','in',('bank','cash'))]"/>
<field name="period_id"/>
<group expand="0" string="Group By...">
@ -34,7 +34,7 @@
<field name="date"/>
<filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Draft Vouchers"/>
<filter icon="terp-camera_test" string="Posted" domain="[('state','=','posted')]" help="Posted Vouchers"/>
<field name="partner_id" string="Supplier"/>
<field name="partner_id" string="Supplier" filter_domain="[('partner_id','child_of',self)]"/>
<field name="journal_id" context="{'journal_id': self, 'set_visible':False}" domain="[('type','in',('bank','cash'))]"/>
<field name="period_id"/>
<group expand="0" string="Group By...">

View File

@ -10,7 +10,7 @@
<field name="date"/>
<filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Draft Vouchers"/>
<filter icon="terp-camera_test" string="Posted" domain="[('state','=','posted')]" help="Posted Vouchers"/>
<field name="partner_id" string="Supplier"/>
<field name="partner_id" string="Supplier" filter_domain="[('partner_id','child_of',self)]"/>
<field name="journal_id" context="{'journal_id': self, 'set_visible':False}" domain="[('type','in',('purchase','purchase_refund'))]"/>
<field name="period_id"/>
<group expand="0" string="Group By...">
@ -32,7 +32,7 @@
<field name="date"/>
<filter icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Draft Vouchers"/>
<filter icon="terp-camera_test" string="Posted" domain="[('state','=','posted')]" help="Posted Vouchers"/>
<field name="partner_id" string="Customer"/>
<field name="partner_id" string="Customer" filter_domain="[('partner_id','child_of',self)]"/>
<field name="journal_id" context="{'journal_id': self, 'set_visible':False}" domain="[('type','in',('sale','sale_refund'))]"/>
<field name="period_id"/>
<group expand="0" string="Group By...">

View File

@ -200,6 +200,9 @@ class res_users(osv.Model):
'partner_id': partner.id,
'email': values.get('email') or values.get('login'),
})
if partner.company_id:
values['company_id'] = partner.company_id.id
values['company_ids'] = [(6,0,[partner.company_id.id])]
self._signup_create_user(cr, uid, values, context=context)
else:
# no token, sign up an external user

View File

@ -20,7 +20,7 @@
<xpath expr="//separator[@string='title']" position="after">
<group colspan="8" height="450" width="750">
<field name="name" invisible="1"/>
<field name="plugin_file" filename="name"/>
<field name="plugin_file" filename="name" widget="url"/>
<separator string="Installation and Configuration Steps" colspan="4"/>
<field name="description" nolabel="1" colspan="8"/>
</group>

View File

@ -23,7 +23,6 @@ from openerp.osv import fields
from openerp.osv import osv
import base64
from openerp.tools.translate import _
from openerp.modules.module import get_module_resource
class base_report_designer_installer(osv.osv_memory):
_name = 'base_report_designer.installer'
@ -31,13 +30,13 @@ class base_report_designer_installer(osv.osv_memory):
def default_get(self, cr, uid, fields, context=None):
data = super(base_report_designer_installer, self).default_get(cr, uid, fields, context=context)
plugin_file = open(get_module_resource('base_report_designer','plugin', 'openerp_report_designer.zip'),'rb')
data['plugin_file'] = base64.encodestring(plugin_file.read())
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
data['plugin_file'] = base_url + '/base_report_designer/static/base-report-designer-plugin/openerp_report_designer.zip'
return data
_columns = {
'name':fields.char('File name', size=34),
'plugin_file':fields.binary('OpenObject Report Designer Plug-in', readonly=True, help="OpenObject Report Designer plug-in file. Save as this file and install this plug-in in OpenOffice."),
'plugin_file':fields.char('OpenObject Report Designer Plug-in', size=256, readonly=True, help="OpenObject Report Designer plug-in file. Save as this file and install this plug-in in OpenOffice."),
'description':fields.text('Description', readonly=True)
}

View File

@ -134,6 +134,9 @@ class res_partner(osv.osv):
'vat_subjected': fields.boolean('VAT Legal Statement', help="Check this box if the partner is subjected to the VAT. It will be used for the VAT legal statement.")
}
def _commercial_fields(self, cr, uid, context=None):
return super(res_partner, self)._commercial_fields(cr, uid, context=context) + ['vat_subjected']
def _construct_constraint_msg(self, cr, uid, ids, context=None):
def default_vat_check(cn, vn):
# by default, a VAT number is valid if:

View File

@ -1053,6 +1053,14 @@ class crm_lead(base_stage, format_address, osv.osv):
message = _("%s a call for %s.%s") % (prefix, phonecall.date, suffix)
return self.message_post(cr, uid, ids, body=message, context=context)
def log_meeting(self, cr, uid, ids, meeting_subject, meeting_date, duration, context=None):
if not duration:
duration = _('unknown')
else:
duration = str(duration)
message = _("Meeting scheduled at '%s'<br> Subject: %s <br> Duration: %s hour(s)") % (meeting_date, meeting_subject, duration)
return self.message_post(cr, uid, ids, body=message, context=context)
def onchange_state(self, cr, uid, ids, state_id, context=None):
if state_id:
country_id=self.pool.get('res.country.state').browse(cr, uid, state_id, context).country_id.id

View File

@ -330,7 +330,7 @@
<field name="categ_ids" string="Category" filter_domain="[('categ_ids','ilike',self)]"/>
<field name="section_id" context="{'invisible_section': False}" groups="base.group_multi_salesteams"/>
<field name="user_id"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="create_date"/>
<field name="country_id" context="{'invisible_country': False}"/>
<separator/>
@ -548,7 +548,7 @@
<field name="categ_ids" string="Category" filter_domain="[('categ_ids','ilike', self)]"/>
<field name="section_id" context="{'invisible_section': False}" groups="base.group_multi_salesteams"/>
<field name="user_id"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<separator/>
<filter string="New" name="new" domain="[('state','=','draft')]" help="New Opportunities"/>
<filter string="In Progress" name="open" domain="[('state','=','open')]" help="Open Opportunities"/>

View File

@ -34,6 +34,13 @@ class crm_meeting(osv.Model):
'opportunity_id': fields.many2one ('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]"),
}
def create(self, cr, uid, vals, context=None):
res = super(crm_meeting, self).create(cr, uid, vals, context=context)
obj = self.browse(cr, uid, res, context=context)
if obj.opportunity_id:
self.pool.get('crm.lead').log_meeting(cr, uid, [obj.opportunity_id.id], obj.name, obj.date, obj.duration, context=context)
return res
class calendar_attendee(osv.osv):
""" Calendar Attendee """

View File

@ -186,7 +186,7 @@
<separator/>
<filter string="Phone Calls Assigned to Me or My Team(s)" icon="terp-personal+" domain="['|', ('section_id.user_id','=',uid), ('user_id', '=', uid)]"
help="Phone Calls Assigned to the current user or with a team having the current user as team leader"/>
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="user_id"/>
<field name="section_id" string="Sales Team"
groups="base.group_multi_salesteams"/>

View File

@ -82,7 +82,7 @@
groups="base.group_multi_salesteams"/>
<field name="user_id" string="Salesperson"/>
<group expand="0" string="Extended Filters...">
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="stage_id" domain="[('section_ids', '=', 'section_id')]" />
<field name="type_id"/>
<field name="channel_id"/>

View File

@ -64,7 +64,7 @@
groups="base.group_multi_salesteams"/>
<field name="user_id" string="Salesperson"/>
<group expand="0" string="Extended Filters...">
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="creation_date"/>
<field name="opening_date"/>

View File

@ -201,7 +201,7 @@
<filter icon="terp-gtk-media-pause" string="Pending" domain="[('state','=','pending')]"/>
<separator/>
<filter string="Unassigned Claims" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Claims" />
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Partner" icon="terp-partner" domain="[]" help="Partner" context="{'group_by':'partner_id'}"/>

View File

@ -65,7 +65,7 @@
<field name="section_id" string="Sales Team" context="{'invisible_section': False}"
groups="base.group_multi_salesteams"/>
<group expand="0" string="Extended Filters...">
<field name="partner_id"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="stage_id" domain="[('section_ids', '=', 'section_id')]"/>
<field name="categ_id" domain="[('object_id.model', '=', 'crm.claim')]"/>
<field name="priority"/>

View File

@ -152,7 +152,7 @@
<separator/>
<filter string="Assigned to Me or My Sales Team(s)" icon="terp-personal+" domain="['|', ('section_id.user_id','=',uid), ('section_id.member_ids', 'in', [uid])]"
help="Helpdesk requests that are assigned to me or to one of the sale teams I manage" />
<field name="partner_id" />
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<field name="user_id"/>
<field name="section_id" string="Sales Team" groups="base.group_multi_salesteams"/>
<group expand="0" string="Group By...">

View File

@ -62,6 +62,7 @@
<field name="user_id" string="Salesperson"/>
<field name="section_id" string="Sales Team" context="{'invisible_section': False}" groups="base.group_multi_salesteams"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/>
<group expand="0" string="Extended Filters..." groups="base.group_no_one">
<field name="priority" string="Priority"/>
<field name="categ_id"/>

View File

@ -4,8 +4,7 @@
Mail script will be fetched him request from mail server. so I process that mail after read EML file
-
!python {model: mail.thread}: |
from openerp import addons
request_file = open(addons.get_module_resource('crm_helpdesk','test', 'customer_question.eml'),'rb')
request_file = open(openerp.modules.module.get_module_resource('crm_helpdesk','test', 'customer_question.eml'),'rb')
request_message = request_file.read()
self.message_process(cr, uid, 'crm.helpdesk', request_message)
-

View File

@ -52,6 +52,9 @@ class hr_expense_expense(osv.osv):
res[expense.id] = total
return res
def _get_expense_from_line(self, cr, uid, ids, context=None):
return [line.expense_id.id for line in self.pool.get('hr.expense.line').browse(cr, uid, ids, context=context)]
def _get_currency(self, cr, uid, context=None):
user = self.pool.get('res.users').browse(cr, uid, [uid], context=context)[0]
if user.company_id:
@ -84,7 +87,10 @@ class hr_expense_expense(osv.osv):
'account_move_id': fields.many2one('account.move', 'Ledger Posting'),
'line_ids': fields.one2many('hr.expense.line', 'expense_id', 'Expense Lines', readonly=True, states={'draft':[('readonly',False)]} ),
'note': fields.text('Note'),
'amount': fields.function(_amount, string='Total Amount', digits_compute=dp.get_precision('Account')),
'amount': fields.function(_amount, string='Total Amount', digits_compute=dp.get_precision('Account'),
store={
'hr.expense.line': (_get_expense_from_line, ['unit_amount','unit_quantity'], 10)
}),
'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'department_id':fields.many2one('hr.department','Department', readonly=True, states={'draft':[('readonly',False)], 'confirm':[('readonly',False)]}),
'company_id': fields.many2one('res.company', 'Company', required=True),

View File

@ -32,7 +32,7 @@
<field name="user_id" invisible="1"/>
<field name="name"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="amount"/>
<field name="amount" sum="Total Amount"/>
<field name="state"/>
</tree>
</field>

View File

@ -510,8 +510,9 @@ class hr_job(osv.osv):
def _auto_init(self, cr, context=None):
"""Installation hook to create aliases for all jobs and avoid constraint errors."""
self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job,self)._auto_init,
res = self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job,self)._auto_init,
self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=context)
return res
def create(self, cr, uid, vals, context=None):
mail_alias = self.pool.get('mail.alias')

View File

@ -19,8 +19,7 @@
An applicant is interested in the job position. So he sends a resume by email.
-
!python {model: mail.thread}: |
from openerp import addons
request_file = open(addons.get_module_resource('hr_recruitment','test', 'resume.eml'),'rb')
request_file = open(openerp.modules.module.get_module_resource('hr_recruitment','test', 'resume.eml'),'rb')
request_message = request_file.read()
self.message_process(cr, uid, 'hr.applicant', request_message)
-

2
addons/im/__init__.py Normal file
View File

@ -0,0 +1,2 @@
import im

27
addons/im/__openerp__.py Normal file
View File

@ -0,0 +1,27 @@
{
'name' : 'Instant Messaging',
'version': '1.0',
'summary': 'Live Chat, Talks with Others',
'sequence': '18',
'category': 'Tools',
'complexity': 'easy',
'description':
"""
Instant Messaging
=================
Allows users to chat with each other in real time. Find other users easily and
chat in real time. It support several chats in parallel.
""",
'data': [
'security/ir.model.access.csv',
'security/im_security.xml',
],
'depends' : ['base'],
'js': ['static/src/js/*.js'],
'css': ['static/src/css/*.css'],
'qweb': ['static/src/xml/*.xml'],
'installable': True,
'auto_install': False,
'application': True,
}

351
addons/im/im.py Normal file
View File

@ -0,0 +1,351 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 openerp
import openerp.tools.config
import openerp.modules.registry
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
import datetime
from openerp.osv import osv, fields
import time
import logging
import json
import select
_logger = logging.getLogger(__name__)
def listen_channel(cr, channel_name, handle_message, check_stop=(lambda: False), check_stop_timer=60.):
"""
Begin a loop, listening on a PostgreSQL channel. This method does never terminate by default, you need to provide a check_stop
callback to do so. This method also assume that all notifications will include a message formated using JSON (see the
corresponding notify_channel() method).
:param db_name: database name
:param channel_name: the name of the PostgreSQL channel to listen
:param handle_message: function that will be called when a message is received. It takes one argument, the message
attached to the notification.
:type handle_message: function (one argument)
:param check_stop: function that will be called periodically (see the check_stop_timer argument). If it returns True
this function will stop to watch the channel.
:type check_stop: function (no arguments)
:param check_stop_timer: The maximum amount of time between calls to check_stop_timer (can be shorter if messages
are received).
"""
try:
conn = cr._cnx
cr.execute("listen " + channel_name + ";")
cr.commit();
stopping = False
while not stopping:
if check_stop():
stopping = True
break
if select.select([conn], [], [], check_stop_timer) == ([],[],[]):
pass
else:
conn.poll()
while conn.notifies:
message = json.loads(conn.notifies.pop().payload)
handle_message(message)
finally:
try:
cr.execute("unlisten " + channel_name + ";")
cr.commit()
except:
pass # can't do anything if that fails
def notify_channel(cr, channel_name, message):
"""
Send a message through a PostgreSQL channel. The message will be formatted using JSON. This method will
commit the given transaction because the notify command in Postgresql seems to work correctly when executed in
a separate transaction (despite what is written in the documentation).
:param cr: The cursor.
:param channel_name: The name of the PostgreSQL channel.
:param message: The message, must be JSON-compatible data.
"""
cr.commit()
cr.execute("notify " + channel_name + ", %s", [json.dumps(message)])
cr.commit()
POLL_TIMER = 30
DISCONNECTION_TIMER = POLL_TIMER + 5
WATCHER_ERROR_DELAY = 10
if openerp.evented:
import gevent
import gevent.event
class ImWatcher(object):
watchers = {}
@staticmethod
def get_watcher(db_name):
if not ImWatcher.watchers.get(db_name):
ImWatcher(db_name)
return ImWatcher.watchers[db_name]
def __init__(self, db_name):
self.db_name = db_name
ImWatcher.watchers[db_name] = self
self.waiting = 0
self.wait_id = 0
self.users = {}
self.users_watch = {}
gevent.spawn(self.loop)
def loop(self):
_logger.info("Begin watching on channel im_channel for database " + self.db_name)
stop = False
while not stop:
try:
registry = openerp.modules.registry.RegistryManager.get(self.db_name)
with registry.cursor() as cr:
listen_channel(cr, "im_channel", self.handle_message, self.check_stop)
stop = True
except:
# if something crash, we wait some time then try again
_logger.exception("Exception during watcher activity")
time.sleep(WATCHER_ERROR_DELAY)
_logger.info("End watching on channel im_channel for database " + self.db_name)
del ImWatcher.watchers[self.db_name]
def handle_message(self, message):
if message["type"] == "message":
for waiter in self.users.get(message["receiver"], {}).values():
waiter.set()
else: #type status
for waiter in self.users_watch.get(message["user"], {}).values():
waiter.set()
def check_stop(self):
return self.waiting == 0
def _get_wait_id(self):
self.wait_id += 1
return self.wait_id
def stop(self, user_id, watch_users, timeout=None):
wait_id = self._get_wait_id()
event = gevent.event.Event()
self.waiting += 1
self.users.setdefault(user_id, {})[wait_id] = event
for watch in watch_users:
self.users_watch.setdefault(watch, {})[wait_id] = event
try:
event.wait(timeout)
finally:
for watch in watch_users:
del self.users_watch[watch][wait_id]
if len(self.users_watch[watch]) == 0:
del self.users_watch[watch]
del self.users[user_id][wait_id]
if len(self.users[user_id]) == 0:
del self.users[user_id]
self.waiting -= 1
class LongPollingController(openerp.addons.web.http.Controller):
_cp_path = '/longpolling/im'
@openerp.addons.web.http.jsonrequest
def poll(self, req, last=None, users_watch=None, db=None, uid=None, password=None, uuid=None):
assert_uuid(uuid)
if not openerp.evented:
raise Exception("Not usable in a server not running gevent")
if db is not None:
req.session._db = db
req.session._uid = uid
req.session._password = password
req.session.model('im.user').im_connect(uuid=uuid, context=req.context)
my_id = req.session.model('im.user').get_by_user_id(uuid or req.session._uid, req.context)["id"]
num = 0
while True:
res = req.session.model('im.message').get_messages(last, users_watch, uuid=uuid, context=req.context)
if num >= 1 or len(res["res"]) > 0:
return res
last = res["last"]
num += 1
ImWatcher.get_watcher(res["dbname"]).stop(my_id, users_watch or [], POLL_TIMER)
@openerp.addons.web.http.jsonrequest
def activated(self, req):
return not not openerp.evented
@openerp.addons.web.http.jsonrequest
def gen_uuid(self, req):
import uuid
return "%s" % uuid.uuid1()
def assert_uuid(uuid):
if not isinstance(uuid, (str, unicode, type(None))):
raise Exception("%s is not a uuid" % uuid)
class im_message(osv.osv):
_name = 'im.message'
_order = "date desc"
_columns = {
'message': fields.char(string="Message", size=200, required=True),
'from_id': fields.many2one("im.user", "From", required= True, ondelete='cascade'),
'to_id': fields.many2one("im.user", "To", required=True, select=True, ondelete='cascade'),
'date': fields.datetime("Date", required=True, select=True),
}
_defaults = {
'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
}
def get_messages(self, cr, uid, last=None, users_watch=None, uuid=None, context=None):
assert_uuid(uuid)
users_watch = users_watch or []
# complex stuff to determine the last message to show
users = self.pool.get("im.user")
my_id = users.get_by_user_id(cr, uid, uuid or uid, context=context)["id"]
c_user = users.browse(cr, openerp.SUPERUSER_ID, my_id, context=context)
if last:
if c_user.im_last_received < last:
users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context)
else:
last = c_user.im_last_received or -1
# how fun it is to always need to reorder results from read
mess_ids = self.search(cr, openerp.SUPERUSER_ID, [['id', '>', last], ['to_id', '=', my_id]], order="id", context=context)
mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "date"], context=context)
index = {}
for i in xrange(len(mess)):
index[mess[i]["id"]] = mess[i]
mess = []
for i in mess_ids:
mess.append(index[i])
if len(mess) > 0:
last = mess[-1]["id"]
users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context)
return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status}
def post(self, cr, uid, message, to_user_id, uuid=None, context=None):
assert_uuid(uuid)
my_id = self.pool.get('im.user').get_by_user_id(cr, uid, uuid or uid)["id"]
self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id, 'to_id': to_user_id}, context=context)
notify_channel(cr, "im_channel", {'type': 'message', 'receiver': to_user_id})
return False
class im_user(osv.osv):
_name = "im.user"
def _im_status(self, cr, uid, ids, something, something_else, context=None):
res = {}
current = datetime.datetime.now()
delta = datetime.timedelta(0, DISCONNECTION_TIMER)
data = self.read(cr, openerp.SUPERUSER_ID, ids, ["im_last_status_update", "im_last_status"], context=context)
for obj in data:
last_update = datetime.datetime.strptime(obj["im_last_status_update"], DEFAULT_SERVER_DATETIME_FORMAT)
res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current
return res
def search_users(self, cr, uid, domain, fields, limit, context=None):
# do not user openerp.SUPERUSER_ID, reserved to normal users
found = self.pool.get('res.users').search(cr, uid, domain, limit=limit, context=context)
found = self.get_by_user_ids(cr, uid, found, context=context)
return self.read(cr, uid, found, fields, context=context)
def im_connect(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
return self._im_change_status(cr, uid, True, uuid, context)
def im_disconnect(self, cr, uid, uuid=None, context=None):
assert_uuid(uuid)
return self._im_change_status(cr, uid, False, uuid, context)
def _im_change_status(self, cr, uid, new_one, uuid=None, context=None):
assert_uuid(uuid)
id = self.get_by_user_id(cr, uid, uuid or uid, context=context)["id"]
current_status = self.read(cr, openerp.SUPERUSER_ID, id, ["im_status"], context=None)["im_status"]
self.write(cr, openerp.SUPERUSER_ID, id, {"im_last_status": new_one,
"im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
if current_status != new_one:
notify_channel(cr, "im_channel", {'type': 'status', 'user': id})
return True
def get_by_user_id(self, cr, uid, id, context=None):
ids = self.get_by_user_ids(cr, uid, [id], context=context)
return ids[0]
def get_by_user_ids(self, cr, uid, ids, context=None):
user_ids = [x for x in ids if isinstance(x, int)]
uuids = [x for x in ids if isinstance(x, (str, unicode))]
users = self.search(cr, openerp.SUPERUSER_ID, ["|", ["user", "in", user_ids], ["uuid", "in", uuids]], context=None)
records = self.read(cr, openerp.SUPERUSER_ID, users, ["user", "uuid"], context=None)
inside = {}
for i in records:
if i["user"]:
inside[i["user"][0]] = True
elif ["uuid"]:
inside[i["uuid"]] = True
not_inside = {}
for i in ids:
if not (i in inside):
not_inside[i] = True
for to_create in not_inside.keys():
if isinstance(to_create, int):
created = self.create(cr, openerp.SUPERUSER_ID, {"user": to_create}, context=context)
records.append({"id": created, "user": [to_create, ""]})
else:
created = self.create(cr, openerp.SUPERUSER_ID, {"uuid": to_create}, context=context)
records.append({"id": created, "uuid": to_create})
return records
def assign_name(self, cr, uid, uuid, name, context=None):
assert_uuid(uuid)
id = self.get_by_user_id(cr, uid, uuid or uid, context=context)["id"]
self.write(cr, openerp.SUPERUSER_ID, id, {"assigned_name": name}, context=context)
return True
def _get_name(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = record.assigned_name
if record.user:
res[record.id] = record.user.name
continue
return res
_columns = {
'name': fields.function(_get_name, type='char', size=200, string="Name", store=True, readonly=True),
'assigned_name': fields.char(string="Assigned Name", size=200, required=False),
'image': fields.related('user', 'image_small', type='binary', string="Image", readonly=True),
'user': fields.many2one("res.users", string="User", select=True, ondelete='cascade'),
'uuid': fields.char(string="UUID", size=50, select=True),
'im_last_received': fields.integer(string="Instant Messaging Last Received Message"),
'im_last_status': fields.boolean(strint="Instant Messaging Last Status"),
'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"),
'im_status': fields.function(_im_status, string="Instant Messaging Status", type='boolean'),
}
_defaults = {
'im_last_received': -1,
'im_last_status': False,
'im_last_status_update': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="message_rule_1" model="ir.rule">
<field name="name">Can only read messages that you sent or messages sent to you</field>
<field name="model_id" ref="model_im_message"/>
<field name="groups" eval="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">["|", ('to_id.user', '=', user.id), ('from_id.user', '=', user.id)]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="users_rule_1" model="ir.rule">
<field name="name">Can only modify your user</field>
<field name="model_id" ref="model_im_user"/>
<field name="groups" eval="[(6,0,[ref('base.group_user')])]"/>
<field name="domain_force">[('user', '=', user.id)]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_create" eval="0"/>
</record>
</data>
</openerp>

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_im_message,im.message,model_im_message,base.group_user,1,0,1,0
access_im_user,im.user,model_im_user,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_im_message im.message model_im_message base.group_user 1 0 1 0
3 access_im_user im.user model_im_user base.group_user 1 1 1 0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,258 @@
.openerp .oe_im {
position: fixed;
background-color: #E8EBEF;
width: 220px;
border-left: 1px solid #AEB9BD;
}
/* button */
.openerp .oe_topbar_imbutton {
cursor: pointer;
}
/* search stuff */
.openerp .oe_im_frame_header {
position: relative;
background: #dedede;
background: -moz-linear-gradient(#fcfcfc, #dedede);
background: -webkit-gradient(linear, left top, left bottom, from(#fcfcfc), to(#dedede));
border-bottom: 1px solid border-color !important;
padding: 5px;
}
.openerp .oe_im_frame_header .oe_im_searchbox {
width: 168px;
padding: 1px 21px 1px 19px;
font-size: 13px;
-moz-border-radius: 13px;
-webkit-border-radius: 13px;
border-radius: 13px;
}
.openerp .oe_im_frame_header .oe_im_search_icon {
position: absolute;
color: #888;
top: 2px;
left: 9px;
font-size: 28px;
font-family: "entypoRegular" !important;
font-weight: 300 !important;
}
.openerp .oe_im_frame_header .oe_im_search_clear {
display: none;
position: absolute;
right: 11px;
top: 4px;
font-size: 26px;
color: #b6b6b6;
cursor: pointer;
}
.openerp .oe_im_frame_header .oe_im_search_clear:hover {
color: #888;
}
/* users */
.openerp .oe_im_users {
padding-bottom: 38px;
}
.openerp .oe_im_user {
position: relative;
padding: 2px 6px;
cursor: pointer;
font-size: 13px;
margin-bottom: 3px;
}
.openerp .oe_im_user:hover {
background: lightGrey;
}
.openerp .oe_im_user_clip {
display: inline-block;
width: 26px;
height: 26px;
margin-right: 4px;
-moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
-webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
}
.openerp .oe_im_user_avatar {
width: 26px;
height: auto;
}
.openerp .oe_im_user_name {
width: 162px;
line-height: 26px;
padding-right: 15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
position: relative;
}
.openerp .oe_im_user_online {
display: none;
position: absolute;
top: 9.5px;
right: 11px;
width: 11px;
height: 11px;
vertical-align: middle;
border: 0;
}
/* conversations */
.openerp .oe_im_chatview {
position: fixed;
overflow: hidden;
bottom: 6px;
margin-right: 6px;
background: rgba(60, 60, 60, 0.8);
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: 0 0 3px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.3);
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
width: 240px;
}
.openerp .oe_im_chatview .oe_im_chatview_disconnected {
display:none;
z-index: 100;
width: 100%;
background: #E8EBEF;
padding: 5px;
font-size: 11px;
color: #999;
line-height: 14px;
height: 28px;
overflow: hidden;
}
.openerp .oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected {
display: block;
}
.openerp .oe_im_chatview .oe_im_chatview_header {
padding: 3px 6px 2px;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
-moz-border-radius: 3px 3px 0 0;
-webkit-border-radius: 3px 3px 0 0;
border-radius: 3px 3px 0 0;
border-bottom: 1px solid #AEB9BD;
cursor: pointer;
}
.openerp .oe_im_chatview .oe_im_chatview_close {
padding: 0;
cursor: pointer;
background: transparent;
border: 0;
-webkit-appearance: none;
font-size: 18px;
line-height: 16px;
float: right;
font-weight: bold;
color: black;
text-shadow: 0 1px 0 white;
opacity: 0.2;
}
.openerp .oe_im_chatview .oe_im_chatview_content {
overflow: auto;
height: 287px;
}
.openerp .oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content {
height: 249px;
}
.openerp .oe_im_chatview .oe_im_chatview_footer {
position: relative;
padding: 3px;
border-top: 1px solid #AEB9BD;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
-moz-border-radius: 0 0 3px 3px;
-webkit-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
}
.openerp .oe_im_chatview .oe_im_chatview_input {
width: 222px;
font-family: Lato, Helvetica, sans-serif;
font-size: 13px;
color: #333;
padding: 1px 5px;
border: 1px solid #AEB9BD;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2);
-webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
}
.openerp .oe_im_chatview .oe_im_chatview_bubble {
background: white;
position: relative;
padding: 3px;
margin: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.openerp .oe_im_chatview .oe_im_chatview_clip {
position: relative;
float: left;
width: 26px;
height: 26px;
margin-right: 4px;
-moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
-webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
}
.openerp .oe_im_chatview .oe_im_chatview_avatar {
float: left;
width: 26px;
height: auto;
clip: rect(0, 26px, 26px, 0);
max-width: 100%;
width: auto 9;
height: auto;
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
}
.openerp .oe_im_chatview .oe_im_chatview_time {
position: absolute;
right: 0px;
top: 0px;
margin: 3px;
text-align: right;
line-height: 13px;
font-size: 11px;
color: #999;
width: 60px;
overflow: hidden;
}
.openerp .oe_im_chatview .oe_im_chatview_from {
margin: 0 0 2px 0;
line-height: 14px;
font-weight: bold;
font-size: 12px;
width: 140px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: #3A87AD;
}
.openerp .oe_im_chatview .oe_im_chatview_bubble_list {
}
.openerp .oe_im_chatview .oe_im_chatview_bubble_item {
margin: 0 0 2px 30px;
line-height: 14px;
word-wrap: break-word;
}
.openerp .oe_im_chatview_online {
display: none;
margin-top: -4px;
width: 11px;
height: 11px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,461 @@
openerp.im = function(instance) {
var USERS_LIMIT = 20;
var ERROR_DELAY = 5000;
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
instance.web.UserMenu.include({
do_update: function(){
var self = this;
this.update_promise.then(function() {
var im = new instance.im.InstantMessaging(self);
im.appendTo(instance.client.$el);
var button = new instance.im.ImTopButton(this);
button.on("clicked", im, im.switch_display);
button.appendTo(instance.webclient.$el.find('.oe_systray'));
});
return this._super.apply(this, arguments);
},
});
instance.im.ImTopButton = instance.web.Widget.extend({
template:'ImTopButton',
events: {
"click": "clicked",
},
clicked: function() {
this.trigger("clicked");
},
});
instance.im.InstantMessaging = instance.web.Widget.extend({
template: "InstantMessaging",
events: {
"keydown .oe_im_searchbox": "input_change",
"keyup .oe_im_searchbox": "input_change",
"change .oe_im_searchbox": "input_change",
},
init: function(parent) {
this._super(parent);
this.shown = false;
this.set("right_offset", 0);
this.set("current_search", "");
this.users = [];
this.c_manager = new instance.im.ConversationManager(this);
this.on("change:right_offset", this.c_manager, _.bind(function() {
this.c_manager.set("right_offset", this.get("right_offset"));
}, this));
this.user_search_dm = new instance.web.DropMisordered();
},
start: function() {
this.$el.css("right", -this.$el.outerWidth());
$(window).scroll(_.bind(this.calc_box, this));
$(window).resize(_.bind(this.calc_box, this));
this.calc_box();
this.on("change:current_search", this, this.search_changed);
this.search_changed();
var self = this;
return this.c_manager.start_polling();
},
calc_box: function() {
var $topbar = instance.client.$(".oe_topbar");
var top = $topbar.offset().top + $topbar.height();
top = Math.max(top - $(window).scrollTop(), 0);
this.$el.css("top", top);
this.$el.css("bottom", 0);
},
input_change: function() {
this.set("current_search", this.$(".oe_im_searchbox").val());
},
search_changed: function(e) {
var users = new instance.web.Model("im.user");
var self = this;
return this.user_search_dm.add(users.call("search_users",
[[["name", "ilike", this.get("current_search")], ["id", "<>", instance.session.uid]],
["name", "user", "uuid", "im_status"], USERS_LIMIT], {context:new instance.web.CompoundContext()})).then(function(result) {
self.c_manager.add_to_user_cache(result);
self.$(".oe_im_input").val("");
var old_users = self.users;
self.users = [];
_.each(result, function(user) {
var widget = new instance.im.UserWidget(self, self.c_manager.get_user(user.id));
widget.appendTo(self.$(".oe_im_users"));
widget.on("activate_user", self, self.activate_user);
self.users.push(widget);
});
_.each(old_users, function(user) {
user.destroy();
});
});
},
switch_display: function() {
var fct = _.bind(function(place) {
this.set("right_offset", place + this.$el.outerWidth());
}, this);
var opt = {
step: fct,
};
if (this.shown) {
this.$el.animate({
right: -this.$el.outerWidth(),
}, opt);
} else {
if (! this.c_manager.get_activated()) {
this.do_warn("Instant Messaging is not activated on this server.", "");
return;
}
this.$el.animate({
right: 0,
}, opt);
}
this.shown = ! this.shown;
},
activate_user: function(user) {
this.c_manager.activate_user(user, true);
},
});
instance.im.UserWidget = instance.web.Widget.extend({
"template": "UserWidget",
events: {
"click": "activate_user",
},
init: function(parent, user) {
this._super(parent);
this.user = user;
this.user.add_watcher();
},
start: function() {
var change_status = function() {
this.$(".oe_im_user_online").toggle(this.user.get("im_status") === true);
};
this.user.on("change:im_status", this, change_status);
change_status.call(this);
},
activate_user: function() {
this.trigger("activate_user", this.user);
},
destroy: function() {
this.user.remove_watcher();
this._super();
},
});
instance.im.ImUser = instance.web.Class.extend(instance.web.PropertiesMixin, {
init: function(parent, user_rec) {
instance.web.PropertiesMixin.init.call(this, parent);
user_rec.image_url = instance.session.url("/im/static/src/img/avatar/avatar.jpeg");
if (user_rec.user)
user_rec.image_url = instance.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: user_rec.user[0]});
this.set(user_rec);
this.set("watcher_count", 0);
this.on("change:watcher_count", this, function() {
if (this.get("watcher_count") === 0)
this.destroy();
});
},
destroy: function() {
this.trigger("destroyed");
instance.web.PropertiesMixin.destroy.call(this);
},
add_watcher: function() {
this.set("watcher_count", this.get("watcher_count") + 1);
},
remove_watcher: function() {
this.set("watcher_count", this.get("watcher_count") - 1);
},
});
instance.im.ConversationManager = instance.web.Controller.extend({
init: function(parent) {
this._super(parent);
this.set("right_offset", 0);
this.conversations = [];
this.users = {};
this.on("change:right_offset", this, this.calc_positions);
this.set("window_focus", true);
this.set("waiting_messages", 0);
this.focus_hdl = _.bind(function() {
this.set("window_focus", true);
}, this);
$(window).bind("focus", this.focus_hdl);
this.blur_hdl = _.bind(function() {
this.set("window_focus", false);
}, this);
$(window).bind("blur", this.blur_hdl);
this.on("change:window_focus", this, this.window_focus_change);
this.window_focus_change();
this.on("change:waiting_messages", this, this.messages_change);
this.messages_change();
this.create_ting();
this.activated = false;
this.users_cache = {};
this.last = null;
this.unload_event_handler = _.bind(this.unload, this);
},
start_polling: function() {
var self = this;
return new instance.web.Model("im.user").call("get_by_user_id", [instance.session.uid]).then(function(my_id) {
self.my_id = my_id["id"];
return self.ensure_users([self.my_id]).then(function() {
var me = self.users_cache[self.my_id];
delete self.users_cache[self.my_id];
self.me = me;
self.rpc("/longpolling/im/activated", {}, {shadow: true}).then(function(activated) {
if (activated) {
self.activated = true;
$(window).on("unload", self.unload_event_handler);
self.poll();
}
}, function(a, e) {
e.preventDefault();
});
});
});
},
unload: function() {
return new instance.web.Model("im.user").call("im_disconnect", [], {context: new instance.web.CompoundContext()});
},
ensure_users: function(user_ids) {
var no_cache = {};
_.each(user_ids, function(el) {
if (! this.users_cache[el])
no_cache[el] = el;
}, this);
var self = this;
if (_.size(no_cache) === 0)
return $.when();
else
return new instance.web.Model("im.user").call("read", [_.values(no_cache), ["name", "user", "uuid", "im_status"]],
{context: new instance.web.CompoundContext()}).then(function(users) {
self.add_to_user_cache(users);
});
},
add_to_user_cache: function(user_recs) {
_.each(user_recs, function(user_rec) {
if (! this.users_cache[user_rec.id]) {
var user = new instance.im.ImUser(this, user_rec);
this.users_cache[user_rec.id] = user;
user.on("destroyed", this, function() {
delete this.users_cache[user_rec.id];
});
}
}, this);
},
get_user: function(user_id) {
return this.users_cache[user_id];
},
poll: function() {
var self = this;
var user_ids = _.map(this.users_cache, function(el) {
return el.get("id");
});
this.rpc("/longpolling/im/poll", {
last: this.last,
users_watch: user_ids,
context: instance.web.pyeval.eval('context', {}),
}, {shadow: true}).then(function(result) {
_.each(result.users_status, function(el) {
if (self.get_user(el.id))
self.get_user(el.id).set(el);
});
self.last = result.last;
var user_ids = _.pluck(_.pluck(result.res, "from_id"), 0);
self.ensure_users(user_ids).then(function() {
_.each(result.res, function(mes) {
var user = self.get_user(mes.from_id[0]);
self.received_message(mes, user);
});
self.poll();
});
}, function(unused, e) {
e.preventDefault();
setTimeout(_.bind(self.poll, self), ERROR_DELAY);
});
},
get_activated: function() {
return this.activated;
},
create_ting: function() {
var kitten = jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined;
this.ting = new Audio(instance.webclient.session.origin + "/im/static/src/audio/" + (kitten ? "purr" : "Ting") +
(new Audio().canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3"));
},
window_focus_change: function() {
if (this.get("window_focus")) {
this.set("waiting_messages", 0);
}
},
messages_change: function() {
if (! instance.webclient.set_title_part)
return;
instance.webclient.set_title_part("aa_im_messages", this.get("waiting_messages") === 0 ? undefined :
_.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));
},
activate_user: function(user, focus) {
var conv = this.users[user.get('id')];
if (! conv) {
conv = new instance.im.Conversation(this, user, this.me);
conv.appendTo(instance.client.$el);
conv.on("destroyed", this, function() {
this.conversations = _.without(this.conversations, conv);
delete this.users[conv.user.get('id')];
this.calc_positions();
});
this.conversations.push(conv);
this.users[user.get('id')] = conv;
this.calc_positions();
}
if (focus)
conv.focus();
return conv;
},
received_message: function(message, user) {
if (! this.get("window_focus")) {
this.set("waiting_messages", this.get("waiting_messages") + 1);
this.ting.play();
this.create_ting();
}
var conv = this.activate_user(user);
conv.received_message(message);
},
calc_positions: function() {
var current = this.get("right_offset");
_.each(_.range(this.conversations.length), function(i) {
this.conversations[i].set("right_position", current);
current += this.conversations[i].$el.outerWidth(true);
}, this);
},
destroy: function() {
$(window).off("unload", this.unload_event_handler);
$(window).unbind("blur", this.blur_hdl);
$(window).unbind("focus", this.focus_hdl);
this._super();
},
});
instance.im.Conversation = instance.web.Widget.extend({
"template": "Conversation",
events: {
"keydown input": "send_message",
"click .oe_im_chatview_close": "destroy",
"click .oe_im_chatview_header": "show_hide",
},
init: function(parent, user, me) {
this._super(parent);
this.me = me;
this.user = user;
this.user.add_watcher();
this.set("right_position", 0);
this.shown = true;
this.set("pending", 0);
},
start: function() {
var change_status = function() {
this.$el.toggleClass("oe_im_chatview_disconnected_status", this.user.get("im_status") === false);
this.$(".oe_im_chatview_online").toggle(this.user.get("im_status") === true);
this._go_bottom();
};
this.user.on("change:im_status", this, change_status);
change_status.call(this);
this.on("change:right_position", this, this.calc_pos);
this.full_height = this.$el.height();
this.calc_pos();
this.on("change:pending", this, _.bind(function() {
if (this.get("pending") === 0) {
this.$(".oe_im_chatview_nbr_messages").text("");
} else {
this.$(".oe_im_chatview_nbr_messages").text("(" + this.get("pending") + ")");
}
}, this));
},
show_hide: function() {
if (this.shown) {
this.$el.animate({
height: this.$(".oe_im_chatview_header").outerHeight(),
});
} else {
this.$el.animate({
height: this.full_height,
});
}
this.shown = ! this.shown;
if (this.shown) {
this.set("pending", 0);
}
},
calc_pos: function() {
this.$el.css("right", this.get("right_position"));
},
received_message: function(message) {
if (this.shown) {
this.set("pending", 0);
} else {
this.set("pending", this.get("pending") + 1);
}
this._add_bubble(this.user, message.message, message.date);
},
send_message: function(e) {
if(e && e.which !== 13) {
return;
}
var mes = this.$("input").val();
this.$("input").val("");
var send_it = _.bind(function() {
var model = new instance.web.Model("im.message");
return model.call("post", [mes, this.user.get('id')],
{context: new instance.web.CompoundContext()});
}, this);
var tries = 0;
send_it().then(_.bind(function() {
this._add_bubble(this.me, mes, instance.web.datetime_to_str(new Date()));
}, this), function(error, e) {
e.preventDefault();
tries += 1;
if (tries < 3)
return send_it();
});
},
_add_bubble: function(user, item, date) {
var items = [item];
if (user === this.last_user) {
this.last_bubble.remove();
items = this.last_items.concat(items);
}
this.last_user = user;
this.last_items = items;
date = instance.web.str_to_datetime(date);
var now = new Date();
var diff = now - date;
if (diff > (1000 * 60 * 60 * 24)) {
date = $.timeago(date);
} else {
date = date.toString(Date.CultureInfo.formatPatterns.shortTime);
}
this.last_bubble = $(QWeb.render("Conversation.bubble", {"items": items, "user": user, "time": date}));
$(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble);
this._go_bottom();
},
_go_bottom: function() {
this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height());
},
focus: function() {
this.$(".oe_im_chatview_input").focus();
},
destroy: function() {
this.user.remove_watcher();
this.trigger("destroyed");
return this._super();
},
});
}

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates xml:space="preserve">
<t t-name="InstantMessaging">
<div class="oe_im">
<div class="oe_im_frame_header">
<span class="oe_e oe_im_search_icon">ô</span>
<input class="oe_im_searchbox" t-att-placeholder="_t('Search users...')"/>
<span class="oe_e oe_im_search_clear">[</span>
</div>
<div class="oe_im_users"></div>
<div class="oe_im_content"></div>
</div>
</t>
<t t-name="ImTopButton">
<div t-att-title='_t("Display Instant Messaging")' class="oe_topbar_item oe_topbar_imbutton">
<span class="oe_e">+</span>
</div>
</t>
<t t-name="UserWidget">
<div class="oe_im_user">
<span class="oe_im_user_clip">
<img t-att-src='widget.user.get("image_url")' class="oe_im_user_avatar"/>
</span>
<span class="oe_im_user_name"><t t-esc="widget.user.get('name')"/></span>
<img t-att-src="_s +'/im/static/src/img/green.png'" class="oe_im_user_online"/>
</div>
</t>
<t t-name="Conversation">
<div class="oe_im_chatview">
<div class="oe_im_chatview_header">
<img t-att-src="_s +'/im/static/src/img/green.png'" class="oe_im_chatview_online"/>
<t t-esc="widget.user.get('name') || 'Anonymous'"/>
<scan class="oe_im_chatview_nbr_messages" />
<button class="oe_im_chatview_close">×</button>
</div>
<div class="oe_im_chatview_disconnected">
<t t-esc='_.str.sprintf(_t("%s is offline. He/She will receive your messages on his/her next connection."), widget.user.get("name") || "Anonymous")'/>
</div>
<div class="oe_im_chatview_content">
<div></div>
</div>
<div class="oe_im_chatview_footer">
<input class="oe_im_chatview_input" t-att-placeholder="_t('Say something...')" />
</div>
</div>
</t>
<t t-name="Conversation.bubble">
<div class="oe_im_chatview_bubble">
<div class="oe_im_chatview_clip">
<img class="oe_im_chatview_avatar" t-att-src='user.get("image_url")'/>
</div>
<div class="oe_im_chatview_from"><t t-esc="user.get('name') || 'Anonymous'"/></div>
<div class="oe_im_chatview_bubble_list">
<t t-foreach="items" t-as="item">
<div class="oe_im_chatview_bubble_item"><t t-esc="item"/></div>
</t>
</div>
<div class="oe_im_chatview_time"><t t-esc="time"/></div>
</div>
</t>
</templates>

View File

@ -0,0 +1,2 @@
import im_livechat

View File

@ -0,0 +1,29 @@
{
'name' : 'Live Support',
'version': '1.0',
'summary': 'Live Chat with Visitors/Customers',
'category': 'Tools',
'complexity': 'easy',
'description':
"""
Live Chat Support
=================
Allow to drop instant messaging widgets on any web page that will communicate
with the current server and dispatch visitors request amongst several live
chat operators.
""",
'data': [
"security/im_livechat_security.xml",
"security/ir.model.access.csv",
"im_livechat_view.xml",
],
'demo': [
"im_livechat_demo.xml",
],
'depends' : ["im", "mail", "portal_anonymous"],
'installable': True,
'auto_install': False,
'application': True,
}

View File

@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 openerp
import openerp.addons.im.im as im
import json
import random
import jinja2
from openerp.osv import osv, fields
from openerp import tools
env = jinja2.Environment(
loader=jinja2.PackageLoader('openerp.addons.im_livechat', "."),
autoescape=False
)
env.filters["json"] = json.dumps
class LiveChatController(openerp.addons.web.http.Controller):
_cp_path = '/im_livechat'
@openerp.addons.web.http.httprequest
def loader(self, req, **kwargs):
p = json.loads(kwargs["p"])
db = p["db"]
channel = p["channel"]
user_name = p.get("user_name", None)
req.session._db = db
req.session._uid = None
req.session._login = "anonymous"
req.session._password = "anonymous"
info = req.session.model('im_livechat.channel').get_info_for_chat_src(channel)
info["db"] = db
info["channel"] = channel
info["userName"] = user_name
return req.make_response(env.get_template("loader.js").render(info),
headers=[('Content-Type', "text/javascript")])
@openerp.addons.web.http.httprequest
def web_page(self, req, **kwargs):
p = json.loads(kwargs["p"])
db = p["db"]
channel = p["channel"]
req.session._db = db
req.session._uid = None
req.session._login = "anonymous"
req.session._password = "anonymous"
script = req.session.model('im_livechat.channel').read(channel, ["script"])["script"]
info = req.session.model('im_livechat.channel').get_info_for_chat_src(channel)
info["script"] = script
return req.make_response(env.get_template("web_page.html").render(info),
headers=[('Content-Type', "text/html")])
@openerp.addons.web.http.jsonrequest
def available(self, req, db, channel):
req.session._db = db
req.session._uid = None
req.session._login = "anonymous"
req.session._password = "anonymous"
return req.session.model('im_livechat.channel').get_available_user(channel) > 0
class im_livechat_channel(osv.osv):
_name = 'im_livechat.channel'
def _get_default_image(self, cr, uid, context=None):
image_path = openerp.modules.get_module_resource('im_livechat', 'static/src/img', 'default.png')
return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
def _get_image(self, cr, uid, ids, name, args, context=None):
result = dict.fromkeys(ids, False)
for obj in self.browse(cr, uid, ids, context=context):
result[obj.id] = tools.image_get_resized_images(obj.image)
return result
def _set_image(self, cr, uid, id, name, value, args, context=None):
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
def _are_you_inside(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = False
for user in record.user_ids:
if user.id == uid:
res[record.id] = True
break
return res
def _script(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = env.get_template("include.html").render({
"url": self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url'),
"parameters": {"db":cr.dbname, "channel":record.id},
})
return res
def _web_page(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') + \
"/im_livechat/web_page?p=" + json.dumps({"db":cr.dbname, "channel":record.id})
return res
_columns = {
'name': fields.char(string="Channel Name", size=200, required=True),
'user_ids': fields.many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string="Users"),
'are_you_inside': fields.function(_are_you_inside, type='boolean', string='Are you inside the matrix?', store=False),
'script': fields.function(_script, type='text', string='Script', store=False),
'web_page': fields.function(_web_page, type='url', string='Web Page', store=False, size="200"),
'button_text': fields.char(string="Text of the Button", size=200),
'input_placeholder': fields.char(string="Chat Input Placeholder", size=200),
'default_message': fields.char(string="Welcome Message", size=200, help="This is an automated 'welcome' message that your visitor will see when they initiate a new chat session."),
# image: all image fields are base64 encoded and PIL-supported
'image': fields.binary("Photo",
help="This field holds the image used as photo for the group, limited to 1024x1024px."),
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
string="Medium-sized photo", type="binary", multi="_get_image",
store={
'im_livechat.channel': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},
help="Medium-sized photo of the group. It is automatically "\
"resized as a 128x128px image, with aspect ratio preserved. "\
"Use this field in form views or some kanban views."),
'image_small': fields.function(_get_image, fnct_inv=_set_image,
string="Small-sized photo", type="binary", multi="_get_image",
store={
'im_livechat.channel': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},
help="Small-sized photo of the group. It is automatically "\
"resized as a 64x64px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required."),
}
def _default_user_ids(self, cr, uid, context=None):
return [(6, 0, [uid])]
_defaults = {
'button_text': "Have a Question? Chat with us.",
'input_placeholder': "How may I help you?",
'default_message': '',
'user_ids': _default_user_ids,
'image': _get_default_image,
}
def get_available_user(self, cr, uid, channel_id, context=None):
channel = self.browse(cr, openerp.SUPERUSER_ID, channel_id, context=context)
users = []
for user in channel.user_ids:
iuid = self.pool.get("im.user").get_by_user_id(cr, uid, user.id, context=context)["id"]
imuser = self.pool.get("im.user").browse(cr, uid, iuid, context=context)
if imuser.im_status:
users.append(imuser)
if len(users) == 0:
return False
return random.choice(users).id
def test_channel(self, cr, uid, channel, context=None):
if not channel:
return {}
return {
'url': self.browse(cr, uid, channel[0], context=context or {}).web_page,
'type': 'ir.actions.act_url'
}
def get_info_for_chat_src(self, cr, uid, channel, context=None):
url = self.pool.get('ir.config_parameter').get_param(cr, openerp.SUPERUSER_ID, 'web.base.url')
chan = self.browse(cr, uid, channel, context=context)
return {
"url": url,
'buttonText': chan.button_text,
'inputPlaceholder': chan.input_placeholder,
'defaultMessage': chan.default_message,
"channelName": chan.name,
}
def join(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'user_ids': [(4, uid)]})
return True
def quit(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'user_ids': [(3, uid)]})
return True
class im_message(osv.osv):
_inherit = 'im.message'
def _support_member(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = False
if record.to_id.user and record.from_id.user:
continue
elif record.to_id.user:
res[record.id] = record.to_id.user.id
elif record.from_id.user:
res[record.id] = record.from_id.user.id
return res
def _customer(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = False
if record.to_id.uuid and record.from_id.uuid:
continue
elif record.to_id.uuid:
res[record.id] = record.to_id.id
elif record.from_id.uuid:
res[record.id] = record.from_id.id
return res
def _direction(self, cr, uid, ids, name, arg, context=None):
res = {}
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = False
if not not record.to_id.user and not not record.from_id.user:
continue
elif not not record.to_id.user:
res[record.id] = "c2s"
elif not not record.from_id.user:
res[record.id] = "s2c"
return res
_columns = {
'support_member_id': fields.function(_support_member, type='many2one', relation='res.users', string='Support Member', store=True, select=True),
'customer_id': fields.function(_customer, type='many2one', relation='im.user', string='Customer', store=True, select=True),
'direction': fields.function(_direction, type="selection", selection=[("s2c", "Support Member to Customer"), ("c2s", "Customer to Support Member")],
string='Direction', store=False),
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="channel_website" model="im_livechat.channel">
<field name="name">YourWebsite.com</field>
<field name="default_message">Hello, how may I help you?</field>
</record>
<record id="group_im_livechat" model="res.groups">
<field name="users" eval="[(4, ref('base.user_demo'))]"/>
</record>
</data>
</openerp>

View File

@ -0,0 +1,146 @@
<?xml version="1.0"?>
<openerp>
<data>
<menuitem id="im_livechat" name="Live Chat" parent="mail.mail_feeds_main" groups="group_im_livechat"/>
<record model="ir.actions.act_window" id="action_support_channels">
<field name="name">Live Chat Channels</field>
<field name="res_model">im_livechat.channel</field>
<field name="view_mode">kanban,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new live chat channel.
</p><p>
You can create channels for each website on which you want
to integrate the live chat widget, allowing you website
visitors to talk in real time with your operators.
</p><p>
Each channel has it's own URL that you can send by email to
your customers in order to start chatting with you.
</p>
</field>
</record>
<menuitem name="Channels" parent="im_livechat" id="support_channels" action="action_support_channels" groups="group_im_livechat"/>
<record model="ir.ui.view" id="support_channel_kanban">
<field name="name">support_channel.kanban</field>
<field name="model">im_livechat.channel</field>
<field name="arch" type="xml">
<kanban>
<field name="name"/>
<field name="web_page"/>
<field name="are_you_inside"/>
<field name="user_ids"/>
<templates>
<t t-name="kanban-box">
<div class="oe_group_image">
<a type="open"><img t-att-src="kanban_image('im_livechat.channel', 'image_medium', record.id.value)" class="oe_group_photo"/></a>
</div>
<div class="oe_group_details">
<h4><a type="open"><field name="name"/></a></h4>
<div class="oe_kanban_footer_left">
<span>
<span class="oe_e">+</span> <t t-esc="(record.user_ids.raw_value || []).length"/>
</span>
</div>
<div class="oe_group_button">
<button t-if="record.are_you_inside.raw_value" name="quit" type="object" class="oe_group_join">Quit</button>
<button t-if="! record.are_you_inside.raw_value" name="join" type="object">Join</button>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="support_channel_form" model="ir.ui.view">
<field name="name">support_channel.form</field>
<field name="model">im_livechat.channel</field>
<field name="arch" type="xml">
<form string="Support Channels" version="7.0">
<sheet>
<field name="image" widget='image' class="oe_avatar oe_left" options='{"preview_image": "image_medium"}'/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. YourWebsite.com"/>
</h1>
<div>
<button type="object" name="join" class="oe_highlight" string="Join Channel" attrs='{"invisible": [["are_you_inside", "=", True]]}'/>
<button type="object" name="quit" string="Leave Channel" attrs='{"invisible": [["are_you_inside", "=", False]]}'/>
<button type="object" name="test_channel" string="Test" attrs='{"invisible": [["web_page", "=", False]]}'/>
<field name="are_you_inside" invisible="1"/>
</div>
</div>
<group>
<group string="Operators">
<field name="user_ids" widget="many2many_kanban" nolabel="1" colspan="2">
<kanban>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div>
<a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
<div class="oe_group_details" style="min-height: 40px">
<img t-att-src="kanban_image('res.users', 'image', record.id.value)"
class="oe_avatar oe_kanban_avatar_smallbox" style="float:left; margin-right: 10px;"/>
<h4><field name="name"/></h4>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</group>
<group string="Options">
<field name="button_text"/>
<field name="input_placeholder"/>
<field name="default_message" placeholder="e.g. Hello, how may I help you?"/>
</group>
</group>
<div attrs='{"invisible": [["web_page", "=", False]]}'>
<separator string="How to use the Live Chat widget?"/>
<p>
Copy and paste this code into your website, within the &amp;lt;head&amp;gt; tag:
</p>
<field name="script" readonly="1" class="oe_tag"/>
<p>
or copy this url and send it by email to your customers or suppliers:
</p>
<field name="web_page" readonly="1" class="oe_tag"/>
</div>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_history">
<field name="name">History</field>
<field name="res_model">im.message</field>
<field name="view_mode">list</field>
<field name="domain">["|", ('to_id.user', '=', None), ('from_id.user', '=', None)]</field>
</record>
<menuitem name="History" parent="im_livechat" id="history" action="action_history" groups="group_im_livechat_manager"/>
<record id="im_message_form" model="ir.ui.view">
<field name="name">im.message.tree</field>
<field name="model">im.message</field>
<field name="arch" type="xml">
<tree string="History">
<field name="date"/>
<field name="support_member_id"/>
<field name="customer_id"/>
<field name="direction"/>
<field name="message"/>
</tree>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,2 @@
<script type="text/javascript" src="{{url}}/im_livechat/static/ext/static/js/require.js"></script>
<script type="text/javascript" src='{{url}}/im_livechat/loader?p={{parameters | json | escape}}'></script>

View File

@ -0,0 +1,24 @@
require.config({
context: "oelivesupport",
baseUrl: {{url | json}} + "/im_livechat/static/ext/static/js",
shim: {
underscore: {
init: function() {
return _.noConflict();
},
},
"jquery.achtung": {
deps: ['jquery'],
},
},
})(["livesupport", "jquery"], function(livesupport, jQuery) {
jQuery.noConflict();
livesupport.main({{url | json}}, {{db | json}}, "anonymous", "anonymous", {{channel | json}}, {
buttonText: {{buttonText | json}},
inputPlaceholder: {{inputPlaceholder | json}},
defaultMessage: {{(defaultMessage or None) | json}},
auto: window.oe_im_livechat_auto || false,
userName: {{userName | json}} || undefined,
});
});

View File

@ -0,0 +1,36 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="module_category_im_livechat" model="ir.module.category">
<field name="name">Live Support</field>
<field name="sequence" eval="20" />
</record>
<record id="group_im_livechat" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category_im_livechat"/>
<field name="comment">The user will be able to join support channels.</field>
</record>
<record id="group_im_livechat_manager" model="res.groups">
<field name="name">Manager</field>
<field name="comment">The user will be able to delete support channels.</field>
<field name="category_id" ref="module_category_im_livechat"/>
<field name="implied_ids" eval="[(4, ref('im_livechat.group_im_livechat'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="message_rule_1" model="ir.rule">
<field name="name">Live Support Managers can read messages from live support</field>
<field name="model_id" ref="im.model_im_message"/>
<field name="groups" eval="[(6,0,[ref('im_livechat.group_im_livechat_manager')])]"/>
<field name="domain_force">["|", ('to_id.user', '=', None), ('from_id.user', '=', None)]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
</data>
</openerp>

View File

@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ls_chann1,im_livechat.channel,model_im_livechat_channel,,1,0,0,0
access_ls_chann2,im_livechat.channel,model_im_livechat_channel,group_im_livechat,1,1,1,0
access_ls_chann3,im_livechat.channel,model_im_livechat_channel,group_im_livechat_manager,1,1,1,1
access_ls_message,im_livechat.im.message,im.model_im_message,portal.group_anonymous,0,0,0,0
access_im_user,im_livechat.im.user,im.model_im_user,portal.group_anonymous,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ls_chann1 im_livechat.channel model_im_livechat_channel 1 0 0 0
3 access_ls_chann2 im_livechat.channel model_im_livechat_channel group_im_livechat 1 1 1 0
4 access_ls_chann3 im_livechat.channel model_im_livechat_channel group_im_livechat_manager 1 1 1 1
5 access_ls_message im_livechat.im.message im.model_im_message portal.group_anonymous 0 0 0 0
6 access_im_user im_livechat.im.user im.model_im_user portal.group_anonymous 1 0 0 0

View File

@ -0,0 +1,3 @@
static/js/livesupport_templates.js: static/js/livesupport_templates.html
python static/js/to_jsonp.py static/js/livesupport_templates.html oe_livesupport_templates_callback > static/js/livesupport_templates.js

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,190 @@
.openerp_style { /* base style of openerp */
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
color: #4c4c4c;
font-size: 13px;
background: white;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5);
}
/* button */
.oe_chat_button {
position: fixed;
bottom: 0px;
right: 6px;
display: inline-block;
min-width: 100px;
background-color: rgba(60, 60, 60, 0.6);
font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif;
font-size: 14px;
font-weight: bold;
padding: 10px;
color: white;
text-shadow: rgb(59, 76, 88) 1px 1px 0px;
border: 1px solid rgb(80, 80, 80);
border-bottom: 0px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
}
/* conversations */
.oe_im_chatview {
position: fixed;
overflow: hidden;
bottom: 42px;
margin-right: 6px;
background: rgba(60, 60, 60, 0.8);
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: 0 0 3px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.3);
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
width: 240px;
}
.oe_im_chatview .oe_im_chatview_disconnected {
display:none;
z-index: 100;
width: 100%;
background: #E8EBEF;
padding: 5px;
font-size: 11px;
color: #999;
line-height: 14px;
height: 28px;
overflow: hidden;
}
.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected {
display: block;
}
.oe_im_chatview .oe_im_chatview_header {
padding: 3px 6px 2px;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
-moz-border-radius: 3px 3px 0 0;
-webkit-border-radius: 3px 3px 0 0;
border-radius: 3px 3px 0 0;
border-bottom: 1px solid #AEB9BD;
cursor: pointer;
}
.oe_im_chatview .oe_im_chatview_close {
padding: 0;
cursor: pointer;
background: transparent;
border: 0;
-webkit-appearance: none;
font-size: 18px;
line-height: 16px;
float: right;
font-weight: bold;
color: black;
text-shadow: 0 1px 0 white;
opacity: 0.2;
}
.oe_im_chatview .oe_im_chatview_content {
overflow: auto;
height: 287px;
width: 240px;
}
.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content {
height: 249px;
}
.oe_im_chatview .oe_im_chatview_footer {
position: relative;
padding: 3px;
border-top: 1px solid #AEB9BD;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
-moz-border-radius: 0 0 3px 3px;
-webkit-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
}
.oe_im_chatview .oe_im_chatview_input {
width: 222px;
font-family: Lato, Helvetica, sans-serif;
font-size: 13px;
color: #333;
padding: 1px 5px;
border: 1px solid #AEB9BD;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2);
-webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
}
.oe_im_chatview .oe_im_chatview_bubble {
background: white;
position: relative;
padding: 3px;
margin: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.oe_im_chatview .oe_im_chatview_clip {
position: relative;
float: left;
width: 26px;
height: 26px;
margin-right: 4px;
-moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
-webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
}
.oe_im_chatview .oe_im_chatview_avatar {
float: left;
width: 26px;
height: auto;
clip: rect(0, 26px, 26px, 0);
max-width: 100%;
width: auto 9;
height: auto;
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
}
.oe_im_chatview .oe_im_chatview_time {
position: absolute;
right: 0px;
top: 0px;
margin: 3px;
text-align: right;
line-height: 13px;
font-size: 11px;
color: #999;
width: 60px;
overflow: hidden;
}
.oe_im_chatview .oe_im_chatview_from {
margin: 0 0 2px 0;
line-height: 14px;
font-weight: bold;
font-size: 12px;
width: 140px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: #3A87AD;
}
.oe_im_chatview .oe_im_chatview_bubble_list {
}
.oe_im_chatview .oe_im_chatview_bubble_item {
margin: 0 0 2px 30px;
line-height: 14px;
word-wrap: break-word;
}
.oe_im_chatview_online {
display: none;
margin-top: -4px;
width: 11px;
height: 11px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,306 @@
/**
* achtung 0.3.0
*
* Growl-like notifications for jQuery
*
* Copyright (c) 2009 Josh Varner <josh@voxwerk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Portions of this file are from the jQuery UI CSS framework.
*
* @license http://www.opensource.org/licenses/mit-license.php
* @author Josh Varner <josh@voxwerk.com>
*/
/* IE 6 doesn't support position: fixed */
* html #achtung-overlay {
position:absolute;
}
/* IE6 includes padding in width */
* html .achtung {
width: 280px;
}
#achtung-overlay {
overflow: hidden;
position: fixed;
top: 15px;
right: 15px;
width: 280px;
z-index:50;
}
.achtung {
display:none;
margin-bottom: 8px;
padding: 15px 15px;
background-color: #000;
color: white;
width: 250px;
font-weight: bold;
position:relative;
overflow: hidden;
-moz-box-shadow: #aaa 1px 1px 2px;
-webkit-box-shadow: #aaa 1px 1px 2px;
box-shadow: #aaa 1px 1px 2px;
-moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px;
/* Note that if using show/hide animations, IE will lose
this setting */
opacity: .85;
filter:Alpha(Opacity=85);
}
/**
* This section from jQuery UI CSS framework
* Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
* Can (and should) be removed if you are already loading the jQuery UI CSS
* to reduce payload size.
*/
.ui-icon { display: block; overflow: hidden; background-repeat: no-repeat; }
.ui-icon { width: 16px; height: 16px; }
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-off { background-position: -96px -144px; }
.ui-icon-radio-on { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
.achtung .achtung-message-icon {
margin-top: 0px;
margin-left: -.5em;
margin-right: .5em;
float: left;
zoom: 1;
}
.achtung .ui-icon.achtung-close-button {
float: right;
margin-right: -8px;
margin-top: -12px;
cursor: pointer;
color: white;
text-align: right;
}
.achtung .ui-icon.achtung-close-button:after {
content: "x"
}
/* Slightly darker for these colors (readability) */
.achtungSuccess, .achtungFail, .achtungWait {
/* Note that if using show/hide animations, IE will lose
this setting */
opacity: .93; filter:Alpha(Opacity=93);
}
.achtungSuccess {
background-color: #4DB559;
}
.achtungFail {
background-color: #D64450;
}
.achtungWait {
background-color: #658093;
}
.achtungSuccess .ui-icon.achtung-close-button,
.achtungFail .ui-icon.achtung-close-button {
}
.achtungSuccess .ui-icon.achtung-close-button-hover,
.achtungFail .ui-icon.achtung-close-button-hover {
}
.achtung .wait-icon {
}
.achtung .achtung-message {
display: inline;
}

View File

@ -0,0 +1,273 @@
/**
* achtung 0.3.0
*
* Growl-like notifications for jQuery
*
* Copyright (c) 2009 Josh Varner <josh@voxwerk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @license http://www.opensource.org/licenses/mit-license.php
* @author Josh Varner <josh@voxwerk.com>
*/
/*globals jQuery,clearTimeout,document,navigator,setTimeout
*/
(function($) {
/**
* This is based on the jQuery UI $.widget code. I would have just made this
* a $.widget but I didn't want the jQuery UI dependency.
*/
$.fn.achtung = function(options)
{
var isMethodCall = (typeof options === 'string'),
args = Array.prototype.slice.call(arguments, 0),
name = 'achtung';
// handle initialization and non-getter methods
return this.each(function() {
var instance = $.data(this, name);
// prevent calls to internal methods
if (isMethodCall && options.substring(0, 1) === '_') {
return this;
}
// constructor
(!instance && !isMethodCall &&
$.data(this, name, new $.achtung(this))._init(args));
// method call
(instance && isMethodCall && $.isFunction(instance[options]) &&
instance[options].apply(instance, args.slice(1)));
});
};
$.achtung = function(element)
{
var args = Array.prototype.slice.call(arguments, 0), $el;
if (!element || !element.nodeType) {
$el = $('<div />');
return $el.achtung.apply($el, args);
}
this.$container = $(element);
};
/**
* Static members
**/
$.extend($.achtung, {
version: '0.3.0',
$overlay: false,
defaults: {
timeout: 10,
disableClose: false,
icon: false,
className: '',
animateClassSwitch: false,
showEffects: {'opacity':'toggle','height':'toggle'},
hideEffects: {'opacity':'toggle','height':'toggle'},
showEffectDuration: 500,
hideEffectDuration: 700
}
});
/**
* Non-static members
**/
$.extend($.achtung.prototype, {
$container: false,
closeTimer: false,
options: {},
_init: function(args)
{
var o, self = this;
args = $.isArray(args) ? args : [];
args.unshift($.achtung.defaults);
args.unshift({});
o = this.options = $.extend.apply($, args);
if (!$.achtung.$overlay) {
$.achtung.$overlay = $('<div id="achtung-overlay"></div>').appendTo(document.body);
}
if (!o.disableClose) {
$('<span class="achtung-close-button ui-icon ui-icon-close" />')
.click(function () { self.close(); })
.hover(function () { $(this).addClass('achtung-close-button-hover'); },
function () { $(this).removeClass('achtung-close-button-hover'); })
.prependTo(this.$container);
}
this.changeIcon(o.icon, true);
if (o.message) {
this.$container.append($('<span class="achtung-message">' + o.message + '</span>'));
}
(o.className && this.$container.addClass(o.className));
(o.css && this.$container.css(o.css));
this.$container
.addClass('achtung')
.appendTo($.achtung.$overlay);
if (o.showEffects) {
this.$container.toggle();
} else {
this.$container.show();
}
if (o.timeout > 0) {
this.timeout(o.timeout);
}
},
timeout: function(timeout)
{
var self = this;
if (this.closeTimer) {
clearTimeout(this.closeTimer);
}
this.closeTimer = setTimeout(function() { self.close(); }, timeout * 1000);
this.options.timeout = timeout;
},
/**
* Change the CSS class associated with this message, using
* a transition if available (not availble in Safari/Webkit).
* If no transition is available, the switch is immediate.
*
* #LATER Check if this has been corrected in Webkit or jQuery UI
* #TODO Make transition time configurable
* @param newClass string Name of new class to associate
*/
changeClass: function(newClass)
{
var self = this;
if (this.options.className === newClass) {
return;
}
this.$container.queue(function() {
if (!self.options.animateClassSwitch ||
/webkit/.test(navigator.userAgent.toLowerCase()) ||
!$.isFunction($.fn.switchClass)) {
self.$container.removeClass(self.options.className);
self.$container.addClass(newClass);
} else {
self.$container.switchClass(self.options.className, newClass, 500);
}
self.options.className = newClass;
self.$container.dequeue();
});
},
changeIcon: function(newIcon, force)
{
var self = this;
if ((force !== true || newIcon === false) && this.options.icon === newIcon) {
return;
}
if (force || this.options.icon === false) {
this.$container.prepend($('<span class="achtung-message-icon ui-icon ' + newIcon + '" />'));
this.options.icon = newIcon;
return;
} else if (newIcon === false) {
this.$container.find('.achtung-message-icon').remove();
this.options.icon = false;
return;
}
this.$container.queue(function() {
var $span = $('.achtung-message-icon', self.$container);
if (!self.options.animateClassSwitch ||
/webkit/.test(navigator.userAgent.toLowerCase()) ||
!$.isFunction($.fn.switchClass)) {
$span.removeClass(self.options.icon);
$span.addClass(newIcon);
} else {
$span.switchClass(self.options.icon, newIcon, 500);
}
self.options.icon = newIcon;
self.$container.dequeue();
});
},
changeMessage: function(newMessage)
{
this.$container.queue(function() {
$('.achtung-message', $(this)).html(newMessage);
$(this).dequeue();
});
},
update: function(options)
{
(options.className && this.changeClass(options.className));
(options.css && this.$container.css(options.css));
(typeof(options.icon) !== 'undefined' && this.changeIcon(options.icon));
(options.message && this.changeMessage(options.message));
(options.timeout && this.timeout(options.timeout));
},
close: function()
{
var o = this.options, $container = this.$container;
if (o.hideEffects) {
this.$container.animate(o.hideEffects, o.hideEffectDuration);
} else {
this.$container.hide();
}
$container.queue(function() {
$container.removeData('achtung');
$container.remove();
if ($.achtung.$overlay && $.achtung.$overlay.is(':empty')) {
$.achtung.$overlay.remove();
$.achtung.$overlay = false;
}
$container.dequeue();
});
}
});
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,446 @@
define(["nova", "underscore", "oeclient", "require", "jquery",
"jquery.achtung"], function(nova, _, oeclient, require, $) {
var livesupport = {};
var templateEngine = new nova.TemplateEngine();
templateEngine.extendEnvironment({"toUrl": _.bind(require.toUrl, require)});
var connection;
var defaultInputPlaceholder;
var userName;
livesupport.main = function(server_url, db, login, password, channel, options) {
var defs = [];
options = options || {};
_.defaults(options, {
buttonText: "Chat with one of our collaborators",
inputPlaceholder: "How may I help you?",
defaultMessage: null,
auto: false,
userName: "Anonymous",
});
defaultInputPlaceholder = options.inputPlaceholder;
userName = options.userName;
defs.push($.ajax({
url: require.toUrl("./livesupport_templates.js"),
jsonp: false,
jsonpCallback: "oe_livesupport_templates_callback",
dataType: "jsonp",
cache: true,
}).then(function(content) {
return templateEngine.loadFileContent(content);
}));
defs.push(add_css("../css/livesupport.css"));
defs.push(add_css("./jquery.achtung.css"));
$.when.apply($, defs).then(function() {
console.log("starting live support customer app");
connection = new oeclient.Connection(new oeclient.JsonpRPCConnector(server_url), db, login, password);
connection.connector.call("/im_livechat/available", {db: db, channel: channel}).then(function(activated) {
if (! activated & ! options.auto)
return;
var button = new livesupport.ChatButton(null, channel, options);
button.appendTo($("body"));
if (options.auto)
button.click();
});
});
};
var add_css = function(relative_file_name) {
var css_def = $.Deferred();
$('<link rel="stylesheet" href="' + require.toUrl(relative_file_name) + '"></link>')
.appendTo($("head")).ready(function() {
css_def.resolve();
});
return css_def.promise();
};
var notification = function(message) {
$.achtung({message: message, timeout: 0, showEffects: false, hideEffects: false});
};
var ERROR_DELAY = 5000;
livesupport.ChatButton = nova.Widget.$extend({
className: "openerp_style oe_chat_button",
events: {
"click": "click",
},
__init__: function(parent, channel, options) {
this.$super(parent);
this.channel = channel;
this.options = options;
this.text = options.buttonText;
},
render: function() {
this.$().append(templateEngine.chatButton({widget: this}));
},
click: function() {
if (! this.manager) {
this.manager = new livesupport.ConversationManager(null);
this.activated_def = this.manager.start_polling();
}
var def = $.Deferred();
$.when(this.activated_def).then(function() {
def.resolve();
}, function() {
def.reject();
});
setTimeout(function() {
def.reject();
}, 5000);
def.then(_.bind(this.chat, this), function() {
notification("It seems the connection to the server is encountering problems, please try again later.");
});
},
chat: function() {
var self = this;
if (this.manager.conversations.length > 0)
return;
connection.getModel("im_livechat.channel").call("get_available_user", [this.channel]).then(function(user_id) {
if (! user_id) {
notification("None of our collaborators seems to be available, please try again later.");
return;
}
self.manager.ensure_users([user_id]).then(function() {
var conv = self.manager.activate_user(self.manager.get_user(user_id), true);
if (self.options.defaultMessage) {
conv.received_message({message: self.options.defaultMessage,
date: oeclient.datetime_to_str(new Date())});
}
});
});
},
});
livesupport.ImUser = nova.Class.$extend({
__include__: [nova.DynamicProperties],
__init__: function(parent, user_rec) {
nova.DynamicProperties.__init__.call(this, parent);
user_rec.image_url = require.toUrl("../img/avatar/avatar.jpeg");
if (user_rec.image)
user_rec.image_url = "data:image/png;base64," + user_rec.image;
this.set(user_rec);
this.set("watcher_count", 0);
this.on("change:watcher_count", this, function() {
if (this.get("watcher_count") === 0)
this.destroy();
});
},
destroy: function() {
this.trigger("destroyed");
nova.DynamicProperties.destroy.call(this);
},
add_watcher: function() {
this.set("watcher_count", this.get("watcher_count") + 1);
},
remove_watcher: function() {
this.set("watcher_count", this.get("watcher_count") - 1);
},
});
livesupport.ConversationManager = nova.Class.$extend({
__include__: [nova.DynamicProperties],
__init__: function(parent) {
nova.DynamicProperties.__init__.call(this, parent);
this.set("right_offset", 0);
this.conversations = [];
this.users = {};
this.on("change:right_offset", this, this.calc_positions);
this.set("window_focus", true);
this.set("waiting_messages", 0);
this.focus_hdl = _.bind(function() {
this.set("window_focus", true);
}, this);
$(window).bind("focus", this.focus_hdl);
this.blur_hdl = _.bind(function() {
this.set("window_focus", false);
}, this);
$(window).bind("blur", this.blur_hdl);
this.on("change:window_focus", this, this.window_focus_change);
this.window_focus_change();
this.on("change:waiting_messages", this, this.messages_change);
this.messages_change();
this.create_ting();
this.activated = false;
this.users_cache = {};
this.last = null;
this.unload_event_handler = _.bind(this.unload, this);
},
start_polling: function() {
var self = this;
var uuid = localStorage["oe_livesupport_uuid"];
var def = $.when(uuid);
if (! uuid) {
def = connection.connector.call("/longpolling/im/gen_uuid", {});
}
return def.then(function(uuid) {
localStorage["oe_livesupport_uuid"] = uuid;
return connection.getModel("im.user").call("get_by_user_id", [uuid]);
}).then(function(my_id) {
self.my_id = my_id["id"];
return connection.getModel("im.user").call("assign_name", [uuid, userName]);
}).then(function() {
return self.ensure_users([self.my_id])
}).then(function() {
var me = self.users_cache[self.my_id];
delete self.users_cache[self.my_id];
self.me = me;
me.set("name", "You");
return connection.connector.call("/longpolling/im/activated", {});
}).then(function(activated) {
if (activated) {
self.activated = true;
$(window).on("unload", self.unload_event_handler);
self.poll();
} else {
return $.Deferred().reject();
}
});
},
unload: function() {
connection.getModel("im.user").call("im_disconnect", [], {uuid: this.me.get("uuid"), context: {}});
},
ensure_users: function(user_ids) {
var no_cache = {};
_.each(user_ids, function(el) {
if (! this.users_cache[el])
no_cache[el] = el;
}, this);
var self = this;
if (_.size(no_cache) === 0)
return $.when();
else
return connection.getModel("im.user").call("read", [_.values(no_cache), []]).then(function(users) {
self.add_to_user_cache(users);
});
},
add_to_user_cache: function(user_recs) {
_.each(user_recs, function(user_rec) {
if (! this.users_cache[user_rec.id]) {
var user = new livesupport.ImUser(this, user_rec);
this.users_cache[user_rec.id] = user;
user.on("destroyed", this, function() {
delete this.users_cache[user_rec.id];
});
}
}, this);
},
get_user: function(user_id) {
return this.users_cache[user_id];
},
poll: function() {
console.debug("live support beggin polling");
var self = this;
var user_ids = _.map(this.users_cache, function(el) {
return el.get("id");
});
connection.connector.call("/longpolling/im/poll", {
last: this.last,
users_watch: user_ids,
db: connection.database,
uid: connection.userId,
password: connection.password,
uuid: self.me.get("uuid"),
}).then(function(result) {
_.each(result.users_status, function(el) {
if (self.get_user(el.id))
self.get_user(el.id).set(el);
});
self.last = result.last;
var user_ids = _.pluck(_.pluck(result.res, "from_id"), 0);
self.ensure_users(user_ids).then(function() {
_.each(result.res, function(mes) {
var user = self.get_user(mes.from_id[0]);
self.received_message(mes, user);
});
self.poll();
});
}, function() {
setTimeout(_.bind(self.poll, self), ERROR_DELAY);
});
},
get_activated: function() {
return this.activated;
},
create_ting: function() {
this.ting = new Audio(new Audio().canPlayType("audio/ogg; codecs=vorbis") ?
require.toUrl("../audio/Ting.ogg") :
require.toUrl("../audio/Ting.mp3")
);
},
window_focus_change: function() {
if (this.get("window_focus")) {
this.set("waiting_messages", 0);
}
},
messages_change: function() {
/*if (! instance.webclient.set_title_part)
return;
instance.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined :
_.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));*/
},
activate_user: function(user, focus) {
var conv = this.users[user.get('id')];
if (! conv) {
conv = new livesupport.Conversation(this, user, this.me);
conv.appendTo($("body"));
conv.on("destroyed", this, function() {
this.conversations = _.without(this.conversations, conv);
delete this.users[conv.user.get('id')];
this.calc_positions();
});
this.conversations.push(conv);
this.users[user.get('id')] = conv;
this.calc_positions();
}
if (focus)
conv.focus();
return conv;
},
received_message: function(message, user) {
if (! this.get("window_focus")) {
this.set("waiting_messages", this.get("waiting_messages") + 1);
this.ting.play();
this.create_ting();
}
var conv = this.activate_user(user);
conv.received_message(message);
},
calc_positions: function() {
var current = this.get("right_offset");
_.each(_.range(this.conversations.length), function(i) {
this.conversations[i].set("right_position", current);
current += this.conversations[i].$().outerWidth(true);
}, this);
},
destroy: function() {
$(window).off("unload", this.unload_event_handler);
$(window).unbind("blur", this.blur_hdl);
$(window).unbind("focus", this.focus_hdl);
nova.DynamicProperties.destroy.call(this);
},
});
livesupport.Conversation = nova.Widget.$extend({
className: "openerp_style oe_im_chatview",
events: {
"keydown input": "send_message",
"click .oe_im_chatview_close": "destroy",
"click .oe_im_chatview_header": "show_hide",
},
__init__: function(parent, user, me) {
this.$super(parent);
this.me = me;
this.user = user;
this.user.add_watcher();
this.set("right_position", 0);
this.shown = true;
this.set("pending", 0);
this.inputPlaceholder = defaultInputPlaceholder;
},
render: function() {
this.$().append(templateEngine.conversation({widget: this}));
var change_status = function() {
this.$().toggleClass("oe_im_chatview_disconnected_status", this.user.get("im_status") === false);
this.$(".oe_im_chatview_online").toggle(this.user.get("im_status") === true);
this._go_bottom();
};
this.user.on("change:im_status", this, change_status);
change_status.call(this);
this.on("change:right_position", this, this.calc_pos);
this.full_height = this.$().height();
this.calc_pos();
this.on("change:pending", this, _.bind(function() {
if (this.get("pending") === 0) {
this.$(".oe_im_chatview_nbr_messages").text("");
} else {
this.$(".oe_im_chatview_nbr_messages").text("(" + this.get("pending") + ")");
}
}, this));
},
show_hide: function() {
if (this.shown) {
this.$().animate({
height: this.$(".oe_im_chatview_header").outerHeight(),
});
} else {
this.$().animate({
height: this.full_height,
});
}
this.shown = ! this.shown;
if (this.shown) {
this.set("pending", 0);
}
},
calc_pos: function() {
this.$().css("right", this.get("right_position"));
},
received_message: function(message) {
if (this.shown) {
this.set("pending", 0);
} else {
this.set("pending", this.get("pending") + 1);
}
this._add_bubble(this.user, message.message, oeclient.str_to_datetime(message.date));
},
send_message: function(e) {
if(e && e.which !== 13) {
return;
}
var mes = this.$("input").val();
this.$("input").val("");
var send_it = _.bind(function() {
var model = connection.getModel("im.message");
return model.call("post", [mes, this.user.get('id')], {uuid: this.me.get("uuid"), context: {}});
}, this);
var tries = 0;
send_it().then(_.bind(function() {
this._add_bubble(this.me, mes, new Date());
}, this), function(error, e) {
tries += 1;
if (tries < 3)
return send_it();
});
},
_add_bubble: function(user, item, date) {
var items = [item];
if (user === this.last_user) {
this.last_bubble.remove();
items = this.last_items.concat(items);
}
this.last_user = user;
this.last_items = items;
var zpad = function(str, size) {
str = "" + str;
return new Array(size - str.length + 1).join('0') + str;
};
date = "" + zpad(date.getHours(), 2) + ":" + zpad(date.getMinutes(), 2);
this.last_bubble = $(templateEngine.conversation_bubble({"items": items, "user": user, "time": date}));
$(this.$(".oe_im_chatview_content").children()[0]).append(this.last_bubble);
this._go_bottom();
},
_go_bottom: function() {
this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height());
},
focus: function() {
this.$(".oe_im_chatview_input").focus();
},
destroy: function() {
this.user.remove_watcher();
this.trigger("destroyed");
return this.$super();
},
});
return livesupport;
});

View File

@ -0,0 +1,36 @@
<%def name="conversation">
<div class="oe_im_chatview_header">
<img src="${toUrl('../img/green.png')}" class="oe_im_chatview_online"/>
${widget.user.get('name') || 'You'}
<button class="oe_im_chatview_close">×</button>
</div>
<div class="oe_im_chatview_disconnected">
${widget.user.get("name") + " is offline. He/She will receive your messages on his/her next connection."}
</div>
<div class="oe_im_chatview_content">
<div></div>
</div>
<div class="oe_im_chatview_footer">
<input class="oe_im_chatview_input" placeholder="${widget.inputPlaceholder}" />
</div>
</%def>
<%def name="conversation_bubble">
<div class="oe_im_chatview_bubble">
<div class="oe_im_chatview_clip">
<img class="oe_im_chatview_avatar" src="${user.get('image_url')}"/>
</div>
<div class="oe_im_chatview_from">${user.get('name') || 'You'}</div>
<div class="oe_im_chatview_bubble_list">
% _.each(items, function(item) {
<div class="oe_im_chatview_bubble_item">${item}</div>
% });
</div>
<div class="oe_im_chatview_time">${time}</div>
</div>
</%def>
<%def name="chatButton">
${widget.text}
</%def>

View File

@ -0,0 +1 @@
window.oe_livesupport_templates_callback("\n<%def name=\"conversation\">\n <div class=\"oe_im_chatview_header\">\n <img src=\"${toUrl('../img/green.png')}\" class=\"oe_im_chatview_online\"/>\n ${widget.user.get('name') || 'You'}\n <button class=\"oe_im_chatview_close\">\u00d7</button>\n </div>\n <div class=\"oe_im_chatview_disconnected\">\n ${widget.user.get(\"name\") + \" is offline. He/She will receive your messages on his/her next connection.\"}\n </div>\n <div class=\"oe_im_chatview_content\">\n <div></div>\n </div>\n <div class=\"oe_im_chatview_footer\">\n <input class=\"oe_im_chatview_input\" placeholder=\"${widget.inputPlaceholder}\" />\n </div>\n</%def>\n\n<%def name=\"conversation_bubble\">\n <div class=\"oe_im_chatview_bubble\">\n <div class=\"oe_im_chatview_clip\">\n <img class=\"oe_im_chatview_avatar\" src=\"${user.get('image_url')}\"/>\n </div>\n <div class=\"oe_im_chatview_from\">${user.get('name') || 'You'}</div>\n <div class=\"oe_im_chatview_bubble_list\">\n % _.each(items, function(item) {\n <div class=\"oe_im_chatview_bubble_item\">${item}</div>\n % });\n </div>\n <div class=\"oe_im_chatview_time\">${time}</div>\n </div>\n</%def>\n\n<%def name=\"chatButton\">\n ${widget.text}\n</%def>");

View File

@ -0,0 +1,988 @@
/*
Copyright (c) 2012, Nicolas Vanhoren
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function() {
if (typeof(define) !== "undefined") { // requirejs
define(["jquery", "underscore"], nova_declare);
} else if (typeof(exports) !== "undefined") { // node
var _ = require("underscore")
_.extend(exports, nova_declare(null, _));
} else { // define global variable 'nova'
nova = nova_declare($, _);
}
function nova_declare($, _) {
var nova = {};
nova.internal = {};
/*
* Modified Armin Ronacher's Classy library.
*
* Defines The Class object. That object can be used to define and inherit classes using
* the $extend() method.
*
* Example:
*
* var Person = nova.Class.$extend({
* __init__: function(isDancing){
* this.dancing = isDancing;
* },
* dance: function(){
* return this.dancing;
* }
* });
*
* The __init__() method act as a constructor. This class can be instancied this way:
*
* var person = new Person(true);
* person.dance();
*
* The Person class can also be extended again:
*
* var Ninja = Person.$extend({
* __init__: function(){
* this.$super( false );
* },
* dance: function(){
* // Call the inherited version of dance()
* return this.$super();
* },
* swingSword: function(){
* return true;
* }
* });
*
* When extending a class, each re-defined method can use this.$super() to call the previous
* implementation of that method.
*/
/**
* Classy - classy classes for JavaScript
*
* :copyright: (c) 2011 by Armin Ronacher.
* :license: BSD.
*/
(function(){
var
context = this,
disable_constructor = false;
/* we check if $super is in use by a class if we can. But first we have to
check if the JavaScript interpreter supports that. This also matches
to false positives later, but that does not do any harm besides slightly
slowing calls down. */
var probe_super = (function(){this.$super();}).toString().indexOf('$super') > 0;
function usesSuper(obj) {
return !probe_super || /\B\$super\b/.test(obj.toString());
}
/* helper function to set the attribute of something to a value or
removes it if the value is undefined. */
function setOrUnset(obj, key, value) {
if (value === undefined)
delete obj[key];
else
obj[key] = value;
}
/* gets the own property of an object */
function getOwnProperty(obj, name) {
return Object.prototype.hasOwnProperty.call(obj, name)
? obj[name] : undefined;
}
/* instanciate a class without calling the constructor */
function cheapNew(cls) {
disable_constructor = true;
var rv = new cls;
disable_constructor = false;
return rv;
}
/* the base class we export */
var Class = function() {};
/* extend functionality */
Class.$extend = function(properties) {
var super_prototype = this.prototype;
/* disable constructors and instanciate prototype. Because the
prototype can't raise an exception when created, we are safe
without a try/finally here. */
var prototype = cheapNew(this);
/* copy all properties of the includes over if there are any */
prototype.__mixin_ids = _.clone(prototype.__mixin_ids || {});
if (properties.__include__)
for (var i = 0, n = properties.__include__.length; i != n; ++i) {
var mixin = properties.__include__[i];
if (mixin instanceof nova.Mixin) {
_.extend(prototype.__mixin_ids, mixin.__mixin_ids);
mixin = mixin.__mixin_properties;
}
for (var name in mixin) {
var value = getOwnProperty(mixin, name);
if (value !== undefined)
prototype[name] = mixin[name];
}
}
/* copy class vars from the superclass */
properties.__classvars__ = properties.__classvars__ || {};
if (prototype.__classvars__)
for (var key in prototype.__classvars__)
if (!properties.__classvars__[key]) {
var value = getOwnProperty(prototype.__classvars__, key);
properties.__classvars__[key] = value;
}
/* copy all properties over to the new prototype */
for (var name in properties) {
var value = getOwnProperty(properties, name);
if (name === '__include__' ||
value === undefined)
continue;
prototype[name] = typeof value === 'function' && usesSuper(value) ?
(function(meth, name) {
return function() {
var old_super = getOwnProperty(this, '$super');
this.$super = super_prototype[name];
try {
return meth.apply(this, arguments);
}
finally {
setOrUnset(this, '$super', old_super);
}
};
})(value, name) : value
}
var class_init = this.__class_init__ || function() {};
var p_class_init = prototype.__class_init__ || function() {};
delete prototype.__class_init__;
var n_class_init = function() {
class_init.apply(null, arguments);
p_class_init.apply(null, arguments);
}
n_class_init(prototype);
/* dummy constructor */
var instance = function() {
if (disable_constructor)
return;
var proper_this = context === this ? cheapNew(arguments.callee) : this;
if (proper_this.__init__)
proper_this.__init__.apply(proper_this, arguments);
proper_this.$class = instance;
return proper_this;
}
/* copy all class vars over of any */
for (var key in properties.__classvars__) {
var value = getOwnProperty(properties.__classvars__, key);
if (value !== undefined)
instance[key] = value;
}
/* copy prototype and constructor over, reattach $extend and
return the class */
instance.prototype = prototype;
instance.constructor = instance;
instance.$extend = this.$extend;
instance.$withData = this.$withData;
instance.__class_init__ = n_class_init;
return instance;
};
/* instanciate with data functionality */
Class.$withData = function(data) {
var rv = cheapNew(this);
for (var key in data) {
var value = getOwnProperty(data, key);
if (value !== undefined)
rv[key] = value;
}
return rv;
};
/* export the class */
this.Class = Class;
}).call(nova);
// end of Armin Ronacher's code
var mixinId = 1;
nova.Mixin = nova.Class.$extend({
__init__: function() {
this.__mixin_properties = {};
this.__mixin_id = mixinId;
mixinId++;
this.__mixin_ids = {};
this.__mixin_ids[this.__mixin_id] = true;
_.each(_.toArray(arguments), function(el) {
if (el instanceof nova.Mixin) {
_.extend(this.__mixin_properties, el.__mixin_properties);
_.extend(this.__mixin_ids, el.__mixin_ids);
} else { // object
_.extend(this.__mixin_properties, el)
}
}, this);
_.extend(this, this.__mixin_properties);
}
});
nova.Interface = nova.Mixin.$extend({
__init__: function() {
var lst = [];
_.each(_.toArray(arguments), function(el) {
if (el instanceof nova.Interface) {
lst.push(el);
} else if (el instanceof nova.Mixin) {
var tmp = new nova.Interface(el.__mixin_properties);
tmp.__mixin_ids = el.__mixin_ids;
lst.push(tmp);
} else { // object
var nprops = {};
_.each(el, function(v, k) {
nprops[k] = function() {
throw new nova.NotImplementedError();
};
});
lst.push(nprops);
}
});
this.$super.apply(this, lst);
}
});
nova.hasMixin = function(object, mixin) {
if (! object)
return false;
return (object.__mixin_ids || {})[mixin.__mixin_id] === true;
};
var ErrorBase = function() {
};
ErrorBase.prototype = new Error();
ErrorBase.$extend = nova.Class.$extend;
ErrorBase.$withData = nova.Class.$withData;
nova.Error = ErrorBase.$extend({
name: "nova.Error",
defaultMessage: "",
__init__: function(message) {
this.message = message || this.defaultMessage;
}
});
nova.NotImplementedError = nova.Error.$extend({
name: "nova.NotImplementedError",
defaultMessage: "This method is not implemented"
});
nova.InvalidArgumentError = nova.Error.$extend({
name: "nova.InvalidArgumentError"
});
/**
* Mixin to express the concept of destroying an object.
* When an object is destroyed, it should release any resource
* it could have reserved before.
*/
nova.Destroyable = new nova.Mixin({
__init__: function() {
this.__destroyableDestroyed = false;
},
/**
* Returns true if destroy() was called on the current object.
*/
isDestroyed : function() {
return this.__destroyableDestroyed;
},
/**
* Inform the object it should destroy itself, releasing any
* resource it could have reserved.
*/
destroy : function() {
this.__destroyableDestroyed = true;
}
});
/**
* Mixin to structure objects' life-cycles folowing a parent-children
* relationship. Each object can a have a parent and multiple children.
* When an object is destroyed, all its children are destroyed too.
*/
nova.Parented = new nova.Mixin(nova.Destroyable, {
__parentedMixin : true,
__init__: function() {
nova.Destroyable.__init__.apply(this);
this.__parentedChildren = [];
this.__parentedParent = null;
},
/**
* Set the parent of the current object. When calling this method, the
* parent will also be informed and will return the current object
* when its getChildren() method is called. If the current object did
* already have a parent, it is unregistered before, which means the
* previous parent will not return the current object anymore when its
* getChildren() method is called.
*/
setParent : function(parent) {
if (this.getParent()) {
if (this.getParent().__parentedMixin) {
this.getParent().__parentedChildren = _.without(this
.getParent().getChildren(), this);
}
}
this.__parentedParent = parent;
if (parent && parent.__parentedMixin) {
parent.__parentedChildren.push(this);
}
},
/**
* Return the current parent of the object (or null).
*/
getParent : function() {
return this.__parentedParent;
},
/**
* Return a list of the children of the current object.
*/
getChildren : function() {
return _.clone(this.__parentedChildren);
},
destroy : function() {
_.each(this.getChildren(), function(el) {
el.destroy();
});
this.setParent(undefined);
nova.Destroyable.destroy.apply(this);
}
});
/*
* Yes, we steal Backbone's events :)
*
* This class just handle the dispatching of events, it is not meant to be extended,
* nor used directly. All integration with parenting and automatic unregistration of
* events is done in the mixin EventDispatcher.
*/
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
nova.internal.Events = nova.Class.$extend({
on : function(events, callback, context) {
var ev;
events = events.split(/\s+/);
var calls = this._callbacks || (this._callbacks = {});
while (ev = events.shift()) {
var list = calls[ev] || (calls[ev] = {});
var tail = list.tail || (list.tail = list.next = {});
tail.callback = callback;
tail.context = context;
list.tail = tail.next = {};
}
return this;
},
off : function(events, callback, context) {
var ev, calls, node;
if (!events) {
delete this._callbacks;
} else if (calls = this._callbacks) {
events = events.split(/\s+/);
while (ev = events.shift()) {
node = calls[ev];
delete calls[ev];
if (!callback || !node)
continue;
while ((node = node.next) && node.next) {
if (node.callback === callback
&& (!context || node.context === context))
continue;
this.on(ev, node.callback, node.context);
}
}
}
return this;
},
callbackList: function() {
var lst = [];
_.each(this._callbacks || {}, function(el, eventName) {
var node = el;
while ((node = node.next) && node.next) {
lst.push([eventName, node.callback, node.context]);
}
});
return lst;
},
trigger : function(events) {
var event, node, calls, tail, args, all, rest;
if (!(calls = this._callbacks))
return this;
all = calls['all'];
(events = events.split(/\s+/)).push(null);
// Save references to the current heads & tails.
while (event = events.shift()) {
if (all)
events.push({
next : all.next,
tail : all.tail,
event : event
});
if (!(node = calls[event]))
continue;
events.push({
next : node.next,
tail : node.tail
});
}
rest = Array.prototype.slice.call(arguments, 1);
while (node = events.pop()) {
tail = node.tail;
args = node.event ? [ node.event ].concat(rest) : rest;
while ((node = node.next) !== tail) {
node.callback.apply(node.context || this, args);
}
}
return this;
}
});
// end of Backbone's events class
nova.EventDispatcher = new nova.Mixin(nova.Parented, {
__eventDispatcherMixin: true,
__init__: function() {
nova.Parented.__init__.apply(this);
this.__edispatcherEvents = new nova.internal.Events();
this.__edispatcherRegisteredEvents = [];
},
on: function(events, dest, func) {
var self = this;
events = events.split(/\s+/);
_.each(events, function(eventName) {
self.__edispatcherEvents.on(eventName, func, dest);
if (dest && dest.__eventDispatcherMixin) {
dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});
}
});
return this;
},
off: function(events, dest, func) {
var self = this;
events = events.split(/\s+/);
_.each(events, function(eventName) {
self.__edispatcherEvents.off(eventName, func, dest);
if (dest && dest.__eventDispatcherMixin) {
dest.__edispatcherRegisteredEvents = _.filter(dest.__edispatcherRegisteredEvents, function(el) {
return !(el.name === eventName && el.func === func && el.source === self);
});
}
});
return this;
},
trigger: function(events) {
this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);
return this;
},
destroy: function() {
var self = this;
_.each(this.__edispatcherRegisteredEvents, function(event) {
event.source.__edispatcherEvents.off(event.name, event.func, self);
});
this.__edispatcherRegisteredEvents = [];
_.each(this.__edispatcherEvents.callbackList(), function(cal) {
this.off(cal[0], cal[2], cal[1]);
}, this);
this.__edispatcherEvents.off();
nova.Parented.destroy.apply(this);
}
});
nova.Properties = new nova.Mixin(nova.EventDispatcher, {
__class_init__: function(proto) {
var props = {};
_.each(proto.__properties || {}, function(v, k) {
props[k] = _.clone(v);
});
_.each(proto, function(v, k) {
if (typeof v === "function") {
var res = /^((?:get)|(?:set))([A-Z]\w*)$/.exec(k);
if (! res)
return;
var name = res[2][0].toLowerCase() + res[2].slice(1);
var prop = props[name] || (props[name] = {});
prop[res[1]] = v;
}
});
proto.__properties = props;
},
__init__: function() {
nova.EventDispatcher.__init__.apply(this);
this.__dynamicProperties = {};
},
set: function(arg1, arg2) {
var self = this;
var map;
if (typeof arg1 === "string") {
map = {};
map[arg1] = arg2;
} else {
map = arg1;
}
var tmp_set = this.__props_setting;
this.__props_setting = false;
_.each(map, function(val, key) {
var prop = self.__properties[key];
if (prop) {
if (! prop.set)
throw new nova.InvalidArgumentError("Property " + key + " does not have a setter method.");
prop.set.call(self, val);
} else {
self.fallbackSet(key, val);
}
});
this.__props_setting = tmp_set;
if (! this.__props_setting && this.__props_setted) {
this.__props_setted = false;
self.trigger("change", self);
}
},
get: function(key) {
var prop = this.__properties[key];
if (prop) {
if (! prop.get)
throw new nova.InvalidArgumentError("Property " + key + " does not have a getter method.");
return prop.get.call(this);
} else {
return this.fallbackGet(key);
}
},
fallbackSet: function(key, val) {
throw new nova.InvalidArgumentError("Property " + key + " is not defined.");
},
fallbackGet: function(key) {
throw new nova.InvalidArgumentError("Property " + key + " is not defined.");
},
trigger: function(name) {
nova.EventDispatcher.trigger.apply(this, arguments);
if (/(\s|^)change\:.*/.exec(name)) {
if (! this.__props_setting)
this.trigger("change");
else
this.__props_setted = true;
}
}
});
nova.DynamicProperties = new nova.Mixin(nova.Properties, {
__init__: function() {
nova.Properties.__init__.apply(this);
this.__dynamicProperties = {};
},
fallbackSet: function(key, val) {
var tmp = this.__dynamicProperties[key];
if (tmp === val)
return;
this.__dynamicProperties[key] = val;
this.trigger("change:" + key, this, {
oldValue: tmp,
newValue: val
});
},
fallbackGet: function(key) {
return this.__dynamicProperties[key];
}
});
nova.Widget = nova.Class.$extend({
__include__ : [nova.DynamicProperties],
tagName: 'div',
className: '',
attributes: {},
events: {},
__init__: function(parent) {
nova.Properties.__init__.apply(this);
this.__widget_element = $(document.createElement(this.tagName));
this.$().addClass(this.className);
_.each(this.attributes, function(val, key) {
this.$().attr(key, val);
}, this);
_.each(this.events, function(val, key) {
key = key.split(" ");
val = _.bind(typeof val === "string" ? this[val] : val, this);
if (key.length > 1) {
this.$().on(key[0], key[1], val);
} else {
this.$().on(key[0], val);
}
}, this);
this.setParent(parent);
},
$: function(attr) {
if (attr)
return this.__widget_element.find.apply(this.__widget_element, arguments);
else
return this.__widget_element;
},
/**
* Destroys the current widget, also destroys all its children before destroying itself.
*/
destroy: function() {
_.each(this.getChildren(), function(el) {
el.destroy();
});
this.$().remove();
nova.Properties.destroy.apply(this);
},
/**
* Renders the current widget and appends it to the given jQuery object or Widget.
*
* @param target A jQuery object or a Widget instance.
*/
appendTo: function(target) {
this.$().appendTo(target);
return this.render();
},
/**
* Renders the current widget and prepends it to the given jQuery object or Widget.
*
* @param target A jQuery object or a Widget instance.
*/
prependTo: function(target) {
this.$().prependTo(target);
return this.render();
},
/**
* Renders the current widget and inserts it after to the given jQuery object or Widget.
*
* @param target A jQuery object or a Widget instance.
*/
insertAfter: function(target) {
this.$().insertAfter(target);
return this.render();
},
/**
* Renders the current widget and inserts it before to the given jQuery object or Widget.
*
* @param target A jQuery object or a Widget instance.
*/
insertBefore: function(target) {
this.$().insertBefore(target);
return this.render();
},
/**
* Renders the current widget and replaces the given jQuery object.
*
* @param target A jQuery object or a Widget instance.
*/
replace: function(target) {
this.$().replace(target);
return this.render();
},
/**
* This is the method to implement to render the Widget.
*/
render: function() {}
});
/*
Nova Template Engine
*/
var escape_ = function(text) {
return JSON.stringify(text);
}
var indent_ = function(txt) {
var tmp = _.map(txt.split("\n"), function(x) { return " " + x; });
tmp.pop();
tmp.push("");
return tmp.join("\n");
};
var tparams = {
def_begin: /<%\s*def\s+(?:name=(?:(?:"(.+?)")|(?:'(.+?)')))\s*>/g,
def_end: /<\/%\s*def\s*>/g,
comment_multi_begin: /<%\s*doc\s*>/g,
comment_multi_end: /<\/%\s*doc\s*>/g,
eval_long_begin: /<%/g,
eval_long_end: /%>/g,
eval_short_begin: /(?:^|\n)[[ \t]*%(?!{)/g,
eval_short_end: /\n|$/g,
escape_begin: /\${/g,
interpolate_begin: /%{/g,
comment_begin: /##/g,
comment_end: /\n|$/g
};
// /<%\s*def\s+(?:name=(?:"(.+?)"))\s*%>([\s\S]*?)<%\s*def\s*%>/g
var allbegin = new RegExp(
"((?:\\\\)*)(" +
"(" + tparams.def_begin.source + ")|" +
"(" + tparams.def_end.source + ")|" +
"(" + tparams.comment_multi_begin.source + ")|" +
"(" + tparams.eval_long_begin.source + ")|" +
"(" + tparams.interpolate_begin.source + ")|" +
"(" + tparams.eval_short_begin.source + ")|" +
"(" + tparams.escape_begin.source + ")|" +
"(" + tparams.comment_begin.source + ")" +
")"
, "g");
allbegin.global = true;
var regexes = {
slashes: 1,
match: 2,
def_begin: 3,
def_name1: 4,
def_name2: 5,
def_end: 6,
comment_multi_begin: 7,
eval_long: 8,
interpolate: 9,
eval_short: 10,
escape: 11,
comment: 12
};
var regex_count = 4;
var compileTemplate = function(text, options) {
options = _.extend({start: 0, indent: true}, options);
start = options.start;
var source = "";
var current = start;
allbegin.lastIndex = current;
var text_end = text.length;
var restart = end;
var found;
var functions = [];
var indent = options.indent ? indent_ : function (txt) { return txt; };
var rmWhite = options.removeWhitespaces ? function(txt) {
if (! txt)
return txt;
txt = _.map(txt.split("\n"), function(x) { return x.trim() });
var last = txt.pop();
txt = _.reject(txt, function(x) { return !x });
txt.push(last);
return txt.join("\n") || "\n";
} : function(x) { return x };
while (found = allbegin.exec(text)) {
var to_add = rmWhite(text.slice(current, found.index));
source += to_add ? "__p+=" + escape_(to_add) + ";\n" : '';
current = found.index;
// slash escaping handling
var slashes = found[regexes.slashes] || "";
var nbr = slashes.length;
var nslash = slashes.slice(0, Math.floor(nbr / 2));
source += nbr !== 0 ? "__p+=" + escape_(nslash) + ";\n" : "";
if (nbr % 2 !== 0) {
source += "__p+=" + escape_(found[regexes.match]) + ";\n";
current = found.index + found[0].length;
allbegin.lastIndex = current;
continue;
}
if (found[regexes.def_begin]) {
var sub_compile = compileTemplate(text, _.extend({}, options, {start: found.index + found[0].length}));
var name = (found[regexes.def_name1] || found[regexes.def_name2]);
source += "var " + name + " = function(context) {\n" + indent(sub_compile.header + sub_compile.source
+ sub_compile.footer) + "}\n";
functions.push(name);
current = sub_compile.end;
} else if (found[regexes.def_end]) {
text_end = found.index;
restart = found.index + found[0].length;
break;
} else if (found[regexes.comment_multi_begin]) {
tparams.comment_multi_end.lastIndex = found.index + found[0].length;
var end = tparams.comment_multi_end.exec(text);
if (!end)
throw new Error("<%doc> without corresponding </%doc>");
current = end.index + end[0].length;
} else if (found[regexes.eval_long]) {
tparams.eval_long_end.lastIndex = found.index + found[0].length;
var end = tparams.eval_long_end.exec(text);
if (!end)
throw new Error("<% without matching %>");
var code = text.slice(found.index + found[0].length, end.index);
code = _(code.split("\n")).chain().map(function(x) { return x.trim() })
.reject(function(x) { return !x }).value().join("\n");
source += code + "\n";
current = end.index + end[0].length;
} else if (found[regexes.interpolate]) {
var braces = /{|}/g;
braces.lastIndex = found.index + found[0].length;
var b_count = 1;
var brace;
while (brace = braces.exec(text)) {
if (brace[0] === "{")
b_count++;
else {
b_count--;
}
if (b_count === 0)
break;
}
if (b_count !== 0)
throw new Error("%{ without a matching }");
source += "__p+=" + text.slice(found.index + found[0].length, brace.index) + ";\n"
current = brace.index + brace[0].length;
} else if (found[regexes.eval_short]) {
tparams.eval_short_end.lastIndex = found.index + found[0].length;
var end = tparams.eval_short_end.exec(text);
if (!end)
throw new Error("impossible state!!");
source += text.slice(found.index + found[0].length, end.index).trim() + "\n";
current = end.index;
} else if (found[regexes.escape]) {
var braces = /{|}/g;
braces.lastIndex = found.index + found[0].length;
var b_count = 1;
var brace;
while (brace = braces.exec(text)) {
if (brace[0] === "{")
b_count++;
else {
b_count--;
}
if (b_count === 0)
break;
}
if (b_count !== 0)
throw new Error("${ without a matching }");
source += "__p+=_.escape(" + text.slice(found.index + found[0].length, brace.index) + ");\n"
current = brace.index + brace[0].length;
} else { // comment
tparams.comment_end.lastIndex = found.index + found[0].length;
var end = tparams.comment_end.exec(text);
if (!end)
throw new Error("impossible state!!");
current = end.index + end[0].length;
}
allbegin.lastIndex = current;
}
var to_add = rmWhite(text.slice(current, text_end));
source += to_add ? "__p+=" + escape_(to_add) + ";\n" : "";
var header = "var __p = ''; var print = function() { __p+=Array.prototype.join.call(arguments, '') };\n" +
"with (context || {}) {\n";
var footer = "}\nreturn __p;\n";
source = indent(source);
return {
header: header,
source: source,
footer: footer,
end: restart,
functions: functions,
};
};
nova.TemplateEngine = nova.Class.$extend({
__init__: function() {
this.resetEnvironment();
this.options = {
includeInDom: $ ? true : false,
indent: true,
removeWhitespaces: true,
};
},
loadFile: function(filename) {
var self = this;
return $.get(filename).pipe(function(content) {
return self.loadFileContent(content);
});
},
loadFileContent: function(file_content) {
var code = this.compileFile(file_content);
if (this.options.includeInDom) {
var varname = _.uniqueId("novajstemplate");
var previous = window[varname];
code = "window." + varname + " = " + code + ";";
var def = $.Deferred();
var script = document.createElement("script");
script.type = "text/javascript";
script.text = code;
$("head")[0].appendChild(script);
$(script).ready(function() {
def.resolve();
});
def.then(_.bind(function() {
var tmp = window[varname];
window[varname] = previous;
this.includeTemplates(tmp);
}, this));
return def;
} else {
console.log("return (" + code + ")(context);");
return this.includeTemplates(new Function('context', "return (" + code + ")(context);"));
}
},
compileFile: function(file_content) {
var result = compileTemplate(file_content, _.extend({}, this.options));
var to_append = "";
_.each(result.functions, function(name) {
to_append += name + ": " + name + ",\n";
}, this);
to_append = this.options.indent ? indent_(to_append) : to_append;
to_append = "return {\n" + to_append + "};\n";
to_append = this.options.indent ? indent_(to_append) : to_append;
var code = "function(context) {\n" + result.header +
result.source + to_append + result.footer + "}\n";
return code;
},
includeTemplates: function(fct) {
var add = _.extend({engine: this}, this._env);
var functions = fct(add);
_.each(functions, function(func, name) {
if (this[name])
throw new Error("The template '" + name + "' is already defined");
this[name] = func;
}, this);
},
buildTemplate: function(text) {
var comp = compileTemplate(text, _.extend({}, this.options));
var result = comp.header + comp.source + comp.footer;
var add = _.extend({engine: this}, this._env);
var func = new Function('context', result);
return function(data) {
return func.call(this, _.extend(add, data));
};
},
eval: function(text, context) {
return this.buildTemplate(text)(context);
},
resetEnvironment: function(nenv) {
this._env = {_: _};
this.extendEnvironment(nenv);
},
extendEnvironment: function(env) {
_.extend(this._env, env || {});
},
});
return nova;
};
})();

View File

@ -0,0 +1,362 @@
/*
Copyright (c) 2013, OpenERP
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
define(["underscore", "jquery", "nova"], function(_, $, nova) {
var oeclient = {};
var genericJsonRPC = function(fct_name, params, fct) {
var data = {
jsonrpc: "2.0",
method: fct_name,
params: params,
id: Math.floor(Math.random()* (1000*1000*1000)),
};
return fct(data).pipe(function(result) {
if (result.error !== undefined) {
console.error("Server application error", result.error);
return $.Deferred().reject("server", result.error);
} else {
return result.result;
}
}, function() {
console.error("JsonRPC communication error", _.toArray(arguments));
var def = $.Deferred();
return def.reject.apply(def, ["communication"].concat(_.toArray(arguments)));
});
};
oeclient.jsonRpc = function(url, fct_name, params, settings) {
return genericJsonRPC(fct_name, params, function(data) {
return $.ajax(url, _.extend({}, settings, {
url: url,
dataType: 'json',
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
}));
});
};
oeclient.jsonpRpc = function(url, fct_name, params, settings) {
return genericJsonRPC(fct_name, params, function(data) {
return $.ajax(url, _.extend({}, settings, {
url: url,
dataType: 'jsonp',
jsonp: 'jsonp',
type: 'GET',
cache: false,
data: {r: JSON.stringify(data)},
}));
});
};
oeclient.Connector = nova.Class.$extend({
getService: function(serviceName) {
return new oeclient.Service(this, serviceName);
},
});
oeclient.JsonRPCConnector = oeclient.Connector.$extend({
__init__: function(url) {
this.url = url;
},
call: function(sub_url, content) {
return oeclient.jsonRpc(this.url + sub_url, "call", content);
},
send: function(serviceName, method, args) {
return this.call("/jsonrpc", {"service": serviceName, "method": method, "args": args});
},
});
oeclient.JsonpRPCConnector = oeclient.JsonRPCConnector.$extend({
call: function(sub_url, content) {
return oeclient.jsonpRpc(this.url + sub_url, "call", content);
},
});
oeclient.Service = nova.Class.$extend({
__init__: function(connector, serviceName) {
this.connector = connector;
this.serviceName = serviceName;
},
call: function(method, args) {
return this.connector.send(this.serviceName, method, args);
},
});
oeclient.AuthenticationError = nova.Error.$extend({
name: "oeclient.AuthenticationError",
defaultMessage: "An error occured during authentication."
});
oeclient.Connection = nova.Class.$extend({
__init__: function(connector, database, login, password, userId) {
this.connector = connector;
this.setLoginInfo(database, login, password, userId);
this.userContext = null;
},
setLoginInfo: function(database, login, password, userId) {
this.database = database;
this.login = login;
this.password = password;
this.userId = userId;
},
checkLogin: function(force) {
force = force === undefined ? true: force;
if (this.userId && ! force)
return $.when();
if (! this.database || ! this.login || ! this.password)
throw new oeclient.AuthenticationError();
return this.getService("common").call("login", [this.database, this.login, this.password])
.then(_.bind(function(result) {
this.userId = result;
if (! this.userId) {
console.error("Authentication failure");
return $.Deferred().reject({message:"Authentication failure"});
}
}, this));
},
getUserContext: function() {
if (! this.userContext) {
return this.getModel("res.users").call("context_get").then(_.bind(function(result) {
this.userContext = result;
return this.userContext;
}, this));
}
return $.when(this.userContext);
},
getModel: function(modelName) {
return new oeclient.Model(this, modelName);
},
getService: function(serviceName) {
return this.connector.getService(serviceName);
},
});
oeclient.Model = nova.Class.$extend({
__init__: function(connection, modelName) {
this.connection = connection;
this.modelName = modelName;
},
call: function(method, args, kw) {
return this.connection.checkLogin().then(_.bind(function() {
return this.connection.getService("object").call("execute_kw", [
this.connection.database,
this.connection.userId,
this.connection.password,
this.modelName,
method,
args || [],
kw || {},
]);
}, this));
},
search_read: function(domain, fields, offset, limit, order, context) {
return this.call("search", [domain || [], offset || 0, limit || false, order || false, context || {}]).then(_.bind(function(record_ids) {
if (! record_ids) {
return [];
}
return this.call("read", [record_ids, fields || [], context || {}]).then(function(records) {
var index = {};
_.each(records, function(r) {
index[r.id] = r;
});
var res = [];
_.each(record_ids, function(id) {
if (index[id])
res.push(index[id]);
});
return res;
});
}, this));
},
});
/**
* Converts a string to a Date javascript object using OpenERP's
* datetime string format (exemple: '2011-12-01 15:12:35').
*
* The time zone is assumed to be UTC (standard for OpenERP 6.1)
* and will be converted to the browser's time zone.
*
* @param {String} str A string representing a datetime.
* @returns {Date}
*/
oeclient.str_to_datetime = function(str) {
if(!str) {
return str;
}
var regex = /^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d(?:\.\d+)?)$/;
var res = regex.exec(str);
if ( !res ) {
throw new Error("'" + str + "' is not a valid datetime");
}
var tmp = new Date();
tmp.setUTCFullYear(parseFloat(res[1]));
tmp.setUTCMonth(parseFloat(res[2]) - 1);
tmp.setUTCDate(parseFloat(res[3]));
tmp.setUTCHours(parseFloat(res[4]));
tmp.setUTCMinutes(parseFloat(res[5]));
tmp.setUTCSeconds(parseFloat(res[6]));
return tmp;
};
/**
* Converts a string to a Date javascript object using OpenERP's
* date string format (exemple: '2011-12-01').
*
* As a date is not subject to time zones, we assume it should be
* represented as a Date javascript object at 00:00:00 in the
* time zone of the browser.
*
* @param {String} str A string representing a date.
* @returns {Date}
*/
oeclient.str_to_date = function(str) {
if(!str) {
return str;
}
var regex = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
var res = regex.exec(str);
if ( !res ) {
throw new Error("'" + str + "' is not a valid date");
}
var tmp = new Date();
tmp.setFullYear(parseFloat(res[1]));
tmp.setMonth(parseFloat(res[2]) - 1);
tmp.setDate(parseFloat(res[3]));
tmp.setHours(0);
tmp.setMinutes(0);
tmp.setSeconds(0);
return tmp;
};
/**
* Converts a string to a Date javascript object using OpenERP's
* time string format (exemple: '15:12:35').
*
* The OpenERP times are supposed to always be naive times. We assume it is
* represented using a javascript Date with a date 1 of January 1970 and a
* time corresponding to the meant time in the browser's time zone.
*
* @param {String} str A string representing a time.
* @returns {Date}
*/
oeclient.str_to_time = function(str) {
if(!str) {
return str;
}
var regex = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)$/;
var res = regex.exec(str);
if ( !res ) {
throw new Error("'" + str + "' is not a valid time");
}
debugger;
var tmp = new Date();
tmp.setFullYear(1970);
tmp.setMonth(0);
tmp.setDate(1);
tmp.setHours(parseFloat(res[1]));
tmp.setMinutes(parseFloat(res[2]));
tmp.setSeconds(parseFloat(res[3]));
return tmp;
};
/*
* Left-pad provided arg 1 with zeroes until reaching size provided by second
* argument.
*
* @param {Number|String} str value to pad
* @param {Number} size size to reach on the final padded value
* @returns {String} padded string
*/
var zpad = function(str, size) {
str = "" + str;
return new Array(size - str.length + 1).join('0') + str;
};
/**
* Converts a Date javascript object to a string using OpenERP's
* datetime string format (exemple: '2011-12-01 15:12:35').
*
* The time zone of the Date object is assumed to be the one of the
* browser and it will be converted to UTC (standard for OpenERP 6.1).
*
* @param {Date} obj
* @returns {String} A string representing a datetime.
*/
oeclient.datetime_to_str = function(obj) {
if (!obj) {
return false;
}
return zpad(obj.getUTCFullYear(),4) + "-" + zpad(obj.getUTCMonth() + 1,2) + "-"
+ zpad(obj.getUTCDate(),2) + " " + zpad(obj.getUTCHours(),2) + ":"
+ zpad(obj.getUTCMinutes(),2) + ":" + zpad(obj.getUTCSeconds(),2);
};
/**
* Converts a Date javascript object to a string using OpenERP's
* date string format (exemple: '2011-12-01').
*
* As a date is not subject to time zones, we assume it should be
* represented as a Date javascript object at 00:00:00 in the
* time zone of the browser.
*
* @param {Date} obj
* @returns {String} A string representing a date.
*/
oeclient.date_to_str = function(obj) {
if (!obj) {
return false;
}
return zpad(obj.getFullYear(),4) + "-" + zpad(obj.getMonth() + 1,2) + "-"
+ zpad(obj.getDate(),2);
};
/**
* Converts a Date javascript object to a string using OpenERP's
* time string format (exemple: '15:12:35').
*
* The OpenERP times are supposed to always be naive times. We assume it is
* represented using a javascript Date with a date 1 of January 1970 and a
* time corresponding to the meant time in the browser's time zone.
*
* @param {Date} obj
* @returns {String} A string representing a time.
*/
oeclient.time_to_str = function(obj) {
if (!obj) {
return false;
}
return zpad(obj.getHours(),2) + ":" + zpad(obj.getMinutes(),2) + ":"
+ zpad(obj.getSeconds(),2);
};
return oeclient;
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
import sys
import json
file_name = sys.argv[1]
function_name = sys.argv[2]
with open(file_name) as file_:
content = file_.read()
print "window.%s(%s);" % (function_name, json.dumps(content))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://localhost/im_livechat/static/ext/static/js/require.js"></script>
<script type="text/javascript" src='http://localhost/im_livechat/loader?p={"db":"test","channel":1}'></script>
</head>
<body style="height:100%; margin:0; padding:0;">
<iframe src="http://openerp.com" height="100%" width=100%"></iframe>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Some files were not shown because too many files have changed in this diff Show More