[MERGE]sync with trunk
bzr revid: sgo@tinyerp.com-20130423052419-igsnmrv6n0m3s1d9
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 & 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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)"/>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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...">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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...">
|
||||
|
|
|
@ -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...">
|
||||
|
|
|
@ -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...">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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'}"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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...">
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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)
|
||||
-
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
-
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
import im
|
|
@ -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,
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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>
|
|
@ -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
|
|
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 74 B |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 830 B |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 100 KiB |
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
import im_livechat
|
|
@ -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,
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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>
|
|
@ -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 &lt;head&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>
|
|
@ -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>
|
|
@ -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,
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 74 B |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 830 B |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 100 KiB |
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
});
|
|
@ -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>
|
|
@ -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>");
|
|
@ -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;
|
||||
};
|
||||
})();
|
|
@ -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;
|
||||
|
||||
});
|
|
@ -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))
|
||||
|
|
@ -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>
|
After Width: | Height: | Size: 16 KiB |