[MERGE] merged v7 branch

bzr revid: qdp-launchpad@openerp.com-20130205155556-b4zw4y3gr9v2eom3
This commit is contained in:
Quentin (OpenERP) 2013-02-05 16:55:56 +01:00
commit 552583b9d8
76 changed files with 884 additions and 672 deletions

View File

@ -3521,7 +3521,7 @@ class account_bank_accounts_wizard(osv.osv_memory):
_columns = {
'acc_name': fields.char('Account Name.', size=64, required=True),
'bank_account_id': fields.many2one('wizard.multi.charts.accounts', 'Bank Account', required=True),
'bank_account_id': fields.many2one('wizard.multi.charts.accounts', 'Bank Account', required=True, ondelete='cascade'),
'currency_id': fields.many2one('res.currency', 'Secondary Currency', help="Forces all moves for this account to have this secondary currency."),
'account_type': fields.selection([('cash','Cash'), ('check','Check'), ('bank','Bank')], 'Account Type', size=32),
}

View File

@ -41,7 +41,7 @@ class bank(osv.osv):
def _prepare_name(self, bank):
"Return the name to use when creating a bank journal"
return (bank.bank_name or '') + ' ' + bank.acc_number
return (bank.bank_name or '') + ' ' + (bank.acc_number or '')
def _prepare_name_get(self, cr, uid, bank_dicts, context=None):
"""Add ability to have %(currency_name)s in the format_layout of res.partner.bank.type"""

View File

@ -256,7 +256,7 @@
</group>
<group>
<field name="move_id" groups="account.group_account_user"/>
<field name="period_id" domain="[('state', '=', 'draft')]" groups="account.group_account_user" widget="selection"/>
<field name="period_id" domain="[('state', '=', 'draft'), ('company_id', '=', company_id)]" groups="account.group_account_user"/>
<field name="company_id" on_change="onchange_company_id(company_id,partner_id,type,invoice_line,currency_id)" widget="selection" groups="base.group_multi_company"/>
</group>
</group>
@ -393,7 +393,7 @@
<field name="company_id" on_change="onchange_company_id(company_id,partner_id,type,invoice_line,currency_id)" widget="selection" groups="base.group_multi_company"/>
<field name="user_id" groups="base.group_user"/>
<field domain="[('partner_id.ref_companies', 'in', [company_id])]" name="partner_bank_id"/>
<field name="period_id" domain="[('state', '=', 'draft')]"
<field name="period_id" domain="[('state', '=', 'draft'), ('company_id', '=', company_id)]"
groups="account.group_account_manager"
string="Accounting Period"
placeholder="force period"/>

View File

@ -347,6 +347,15 @@
res_model="account.move.line"
src_model="account.account"/>
<!-- Enable drill-down from Chart Of Accounts tree view -->
<act_window
id="action_account_items"
name="Journal Items"
context="{'search_default_account_id': [active_id]}"
res_model="account.move.line"
src_model="account.account"
key2="tree_but_open"/>
<!-- Account Journal -->
<record id="view_account_journal_tree" model="ir.ui.view">
<field name="name">account.journal.tree</field>
@ -841,6 +850,16 @@
res_model="account.move.line"
src_model="account.tax.code"/>
<!-- Enable drill-down from Chart Of Taxes tree view -->
<act_window
id="action_tax_code_items"
name="Journal Items"
domain="[('tax_code_id','child_of',active_id),('state','!=','draft')]"
res_model="account.move.line"
src_model="account.tax.code"
key2="tree_but_open"/>
<!-- Tax -->
<record id="view_tax_tree" model="ir.ui.view">
<field name="name">account.tax.tree</field>

View File

@ -155,11 +155,13 @@
<record id="mt_invoice_validated" model="mail.message.subtype">
<field name="name">Validated</field>
<field name="res_model">account.invoice</field>
<field name="default" eval="False"/>
<field name="description">Invoice validated</field>
</record>
<record id="mt_invoice_paid" model="mail.message.subtype">
<field name="name">Paid</field>
<field name="res_model">account.invoice</field>
<field name="default" eval="False"/>
<field name="description">Invoice paid</field>
</record>
</data>

View File

@ -12,9 +12,9 @@
<field name="partner_id"/>
<field name="code"/>
<field name="date"/>
<field name="date_start" invisible="1"/>
<field name="date_start"/>
<field name="user_id" invisible="1"/>
<field name="manager_id" invisible="1"/>
<field name="manager_id"/>
<field name="parent_id" invisible="1"/>
<field name="state" invisible="1"/>
<field name="type" invisible="1"/>
@ -30,15 +30,18 @@
<search string="Analytic Account">
<field name="name" filter_domain="['|', ('name','ilike',self), ('code','ilike',self)]" string="Analytic Account"/>
<field name="date"/>
<filter icon="terp-gtk-media-pause" string="Pending" domain="[('state','=','pending')]" help="Pending Accounts"/>
<filter icon="terp-camera_test" string="Current" domain="[('state','=','open')]" help="Current Accounts"/>
<field name="partner_id"/>
<field name="manager_id"/>
<field name="parent_id"/>
<field name="user_id"/>
<filter string="Open" domain="[('state','=','open')]" help="Current Accounts"/>
<filter string="Pending" domain="[('state','=','pending')]" help="Pending Accounts"/>
<group expand="0" string="Group By...">
<filter string="Associated Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Parent Account" icon="terp-folder-green" domain="[]" context="{'group_by':'parent_id'}"/>
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}" groups="base.group_no_one"/>
<filter string="Associated Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Type" domain="[]" context="{'group_by':'type'}"/>
<filter string="Template" domain="[]" context="{'group_by':'template_id'}"/>
<filter string="Parent Account" domain="[]" context="{'group_by':'parent_id'}"/>
<filter string="Status" domain="[]" context="{'group_by':'state'}" groups="base.group_no_one"/>
</group>
</search>
</field>

View File

@ -58,6 +58,7 @@
<separator/>
<filter icon="terp-personal" string="Customer" name="customer" domain="['|', ('type','=','out_invoice'),('type','=','out_refund')]" help="Customer Invoices And Refunds"/>
<filter icon="terp-personal" string="Supplier" domain="['|', ('type','=','in_invoice'),('type','=','in_refund')]" help="Supplier Invoices And Refunds"/>
<separator/>
<filter icon="terp-dolar" string="Invoice" domain="['|', ('type','=','out_invoice'),('type','=','in_invoice')]" help="Customer And Supplier Invoices"/>
<filter icon="terp-dolar_ok!" string="Refund" domain="['|', ('type','=','out_refund'),('type','=','in_refund')]" help="Customer And Supplier Refunds"/>
<field name="partner_id"/>

View File

@ -35,10 +35,7 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header):
'lines': self.lines,
'sum_debit_partner': self._sum_debit_partner,
'sum_credit_partner': self._sum_credit_partner,
# 'sum_debit': self._sum_debit,
# 'sum_credit': self._sum_credit,
'get_currency': self._get_currency,
'comma_me': self.comma_me,
'get_start_period': self.get_start_period,
'get_end_period': self.get_end_period,
'get_account': self._get_account,
@ -78,11 +75,6 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header):
move_state = ['draft','posted']
if self.target_move == 'posted':
move_state = ['posted']
if (data['model'] == 'res.partner'):
## Si on imprime depuis les partenaires
if ids:
PARTNER_REQUEST = "AND line.partner_id IN %s",(tuple(ids),)
if self.result_selection == 'supplier':
self.ACCOUNT_TYPE = ['payable']
elif self.result_selection == 'customer':
@ -98,7 +90,11 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header):
'WHERE a.type IN %s' \
"AND a.active", (tuple(self.ACCOUNT_TYPE), ))
self.account_ids = [a for (a,) in self.cr.fetchall()]
partner_to_use = []
params = [tuple(move_state), tuple(self.account_ids)]
#if we print from the partners, add a clause on active_ids
if (data['model'] == 'res.partner') and ids:
PARTNER_REQUEST = "AND l.partner_id IN %s"
params += [tuple(ids)]
self.cr.execute(
"SELECT DISTINCT l.partner_id " \
"FROM account_move_line AS l, account_account AS account, " \
@ -110,30 +106,10 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header):
# "AND " + self.query +" " \
"AND l.account_id IN %s " \
" " + PARTNER_REQUEST + " " \
"AND account.active ",
(tuple(move_state), tuple(self.account_ids),))
res = self.cr.dictfetchall()
for res_line in res:
partner_to_use.append(res_line['partner_id'])
new_ids = partner_to_use
self.partner_ids = new_ids
objects = obj_partner.browse(self.cr, self.uid, new_ids)
return super(third_party_ledger, self).set_context(objects, data, new_ids, report_type)
def comma_me(self, amount):
if type(amount) is float:
amount = str('%.2f'%amount)
else:
amount = str(amount)
if (amount == '0'):
return ' '
orig = amount
new = re.sub("^(-?\d+)(\d{3})", "\g<1>'\g<2>", amount)
if orig == new:
return new
else:
return self.comma_me(new)
"AND account.active ", params)
self.partner_ids = [res['partner_id'] for res in self.cr.dictfetchall()]
objects = obj_partner.browse(self.cr, self.uid, self.partner_ids)
return super(third_party_ledger, self).set_context(objects, data, self.partner_ids, report_type)
def lines(self, partner):
move_state = ['draft','posted']
@ -290,105 +266,6 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header):
result_tmp = result_tmp + 0.0
return result_tmp + result_init
# code is deprecated
# def _sum_debit(self):
# move_state = ['draft','posted']
# if self.target_move == 'posted':
# move_state = ['posted']
#
# if not self.ids:
# return 0.0
# result_tmp = 0.0
# result_init = 0.0
# if self.reconcil:
# RECONCILE_TAG = " "
# else:
# RECONCILE_TAG = "AND reconcile_id IS NULL"
# if self.initial_balance:
# self.cr.execute(
# "SELECT sum(debit) " \
# "FROM account_move_line AS l, " \
# "account_move AS m "
# "WHERE partner_id IN %s" \
# "AND m.id = l.move_id " \
# "AND m.state IN %s "
# "AND account_id IN %s" \
# "AND reconcile_id IS NULL " \
# "AND " + self.init_query + " ",
# (tuple(self.partner_ids), tuple(move_state), tuple(self.account_ids)))
# contemp = self.cr.fetchone()
# if contemp != None:
# result_init = contemp[0] or 0.0
# else:
# result_init = result_tmp + 0.0
#
# self.cr.execute(
# "SELECT sum(debit) " \
# "FROM account_move_line AS l, " \
# "account_move AS m "
# "WHERE partner_id IN %s" \
# "AND m.id = l.move_id " \
# "AND m.state IN %s "
# "AND account_id IN %s" \
# " " + RECONCILE_TAG + " " \
# "AND " + self.query + " ",
# (tuple(self.partner_ids), tuple(move_state) ,tuple(self.account_ids),))
# contemp = self.cr.fetchone()
# if contemp != None:
# result_tmp = contemp[0] or 0.0
# else:
# result_tmp = result_tmp + 0.0
# return result_tmp + result_init
#
# def _sum_credit(self):
# move_state = ['draft','posted']
# if self.target_move == 'posted':
# move_state = ['posted']
#
# if not self.ids:
# return 0.0
# result_tmp = 0.0
# result_init = 0.0
# if self.reconcil:
# RECONCILE_TAG = " "
# else:
# RECONCILE_TAG = "AND reconcile_id IS NULL"
# if self.initial_balance:
# self.cr.execute(
# "SELECT sum(credit) " \
# "FROM account_move_line AS l, " \
# "account_move AS m "
# "WHERE partner_id IN %s" \
# "AND m.id = l.move_id " \
# "AND m.state IN %s "
# "AND account_id IN %s" \
# "AND reconcile_id IS NULL " \
# "AND " + self.init_query + " ",
# (tuple(self.partner_ids), tuple(move_state), tuple(self.account_ids)))
# contemp = self.cr.fetchone()
# if contemp != None:
# result_init = contemp[0] or 0.0
# else:
# result_init = result_tmp + 0.0
#
# self.cr.execute(
# "SELECT sum(credit) " \
# "FROM account_move_line AS l, " \
# "account_move AS m "
# "WHERE partner_id IN %s" \
# "AND m.id = l.move_id " \
# "AND m.state IN %s "
# "AND account_id IN %s" \
# " " + RECONCILE_TAG + " " \
# "AND " + self.query + " ",
# (tuple(self.partner_ids), tuple(move_state), tuple(self.account_ids),))
# contemp = self.cr.fetchone()
# if contemp != None:
# result_tmp = contemp[0] or 0.0
# else:
# result_tmp = result_tmp + 0.0
# return result_tmp + result_init
def _get_partners(self):
if self.result_selection == 'customer':
return 'Receivable Accounts'

View File

@ -23,7 +23,7 @@
</record>
<record id="action_account_partner_ledger" model="ir.actions.act_window">
<field name="name">Select Period</field>
<field name="name">Partner Ledger</field>
<field name="res_model">account.partner.ledger</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
@ -33,6 +33,13 @@
<field name="target">new</field>
</record>
<record model="ir.values" id="ir_values_account_partner_ledger">
<field name="key2" eval="'client_print_multi'"/>
<field name="model" eval="'res.partner'"/>
<field name="name">Print Partner Ledger</field>
<field name="value" eval="'ir.actions.act_window,%d'%action_account_partner_ledger"/>
</record>
<menuitem icon="STOCK_PRINT"
name="Partner Ledger"
action="action_account_partner_ledger"

View File

@ -25,19 +25,20 @@
<field name="name">account.analytic.account.search</field>
<field name="model">account.analytic.account</field>
<field name="arch" type="xml">
<search string="Analytic Account">
<search string="Contracts">
<field name="name" filter_domain="['|', ('name','ilike',self),('code','ilike',self)]" string="Contract"/>
<field name="date"/>
<filter icon="terp-camera_test" name="open" string="Open" domain="[('state','in',('open','draft'))]" help="Contracts in progress"/>
<filter icon="terp-gtk-media-pause" name="pending" string="Pending" domain="[('state','=','pending')]" help="Pending contracts to renew with your customer"/>
<filter icon="terp-go-today" string="To Renew" domain="['|', '&amp;', ('date', '!=', False), ('date', '&lt;=', time.strftime('%%Y-%%m-%%d')), ('is_overdue_quantity', '=', True)]" name="renew"
help="The contracts to be renewed because the deadline is passed or the working hours are higher than the allocated hours" />
<separator/>
<filter string="Customer Contracts" help="A contract in OpenERP is an analytic account having a partner set on it." name="has_partner" domain="[('partner_id', '!=', False)]" icon="terp-partner" />
<separator/>
<filter string="Contracts not assigned" help="Contracts that are not assigned to an account manager." domain="[('manager_id', '=', False)]" icon="terp-personal-" />
<field name="partner_id"/>
<field name="manager_id"/>
<field name="parent_id"/>
<filter name="open" string="Open" domain="[('state','in',('open','draft'))]" help="Contracts in progress"/>
<filter name="pending" string="Pending" domain="[('state','=','pending')]" help="Pending contracts to renew with your customer"/>
<filter string="To Renew" domain="['|', '&amp;', ('date', '!=', False), ('date', '&lt;=', time.strftime('%%Y-%%m-%%d')), ('is_overdue_quantity', '=', True)]" name="renew"
help="The contracts to be renewed because the deadline is passed or the working hours are higher than the allocated hours" />
<separator/>
<filter string="Customer Contracts" help="Contracts assigned to a customer." name="has_partner" domain="[('partner_id', '!=', False)]"/>
<filter string="Contracts not assigned" help="Contracts that are not assigned to an account manager." domain="[('manager_id', '=', False)]"/>
<separator/>
<group expand="0" string="Group By...">
<filter string="Status" domain="[]" context="{'group_by':'state'}"/>
<filter string="Account Manager" domain="[]" context="{'group_by':'manager_id'}"/>

View File

@ -42,7 +42,7 @@ This module manages:
'category': 'Accounting & Finance',
'sequence': 4,
'website' : 'http://openerp.com',
'images' : ['images/customer_payment.jpeg','images/journal_voucher.jpeg','images/sales_receipt.jpeg','images/supplier_voucher.jpeg'],
'images' : ['images/customer_payment.jpeg','images/journal_voucher.jpeg','images/sales_receipt.jpeg','images/supplier_voucher.jpeg','images/customer_invoice.jpeg','images/customer_refunds.jpeg'],
'depends' : ['account'],
'demo' : [],
'data' : [

View File

@ -962,7 +962,9 @@ class account_voucher(osv.osv):
if not voucher_brw.journal_id.sequence_id.active:
raise osv.except_osv(_('Configuration Error !'),
_('Please activate the sequence of selected journal !'))
name = seq_obj.next_by_id(cr, uid, voucher_brw.journal_id.sequence_id.id, context=context)
c = dict(context)
c.update({'fiscalyear_id': voucher_brw.period_id.fiscalyear_id.id})
name = seq_obj.next_by_id(cr, uid, voucher_brw.journal_id.sequence_id.id, context=c)
else:
raise osv.except_osv(_('Error!'),
_('Please define a sequence on the journal.'))
@ -977,7 +979,7 @@ class account_voucher(osv.osv):
'narration': voucher_brw.narration,
'date': voucher_brw.date,
'ref': ref,
'period_id': voucher_brw.period_id and voucher_brw.period_id.id or False
'period_id': voucher_brw.period_id.id,
}
return move

View File

@ -17,6 +17,7 @@
<record id="mt_voucher_state_change" model="mail.message.subtype">
<field name="name">Status Change</field>
<field name="res_model">account.voucher</field>
<field name="default" eval="False"/>
<field name="description">Status changed</field>
</record>

View File

@ -172,7 +172,7 @@ class account_analytic_account(osv.osv):
_columns = {
'name': fields.char('Account/Contract Name', size=128, required=True),
'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'),
'complete_name': fields.function(_get_full_name, type='char', string='Full Name'),
'code': fields.char('Reference', select=True),
'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True,
help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\

View File

@ -6,16 +6,19 @@
<record id="mt_account_pending" model="mail.message.subtype">
<field name="name">Contract to Renew</field>
<field name="res_model">account.analytic.account</field>
<field name="default" eval="False"/>
<field name="description">Contract pending</field>
</record>
<record id="mt_account_closed" model="mail.message.subtype">
<field name="name">Contract Finished</field>
<field name="res_model">account.analytic.account</field>
<field name="default" eval="False"/>
<field name="description">Contract closed</field>
</record>
<record id="mt_account_opened" model="mail.message.subtype">
<field name="name">Contract Opened</field>
<field name="res_model">account.analytic.account</field>
<field name="default" eval="False"/>
<field name="description">Contract opened</field>
</record>

View File

@ -549,7 +549,7 @@ class ir_model_fields_anonymize_wizard(osv.osv_memory):
key = (line['model_id'], line['field_id'])
custom_updates = fixes.get(key)
if custom_updates:
custom_updates.sort(itemgetter('sequence'))
custom_updates.sort(key=itemgetter('sequence'))
queries = [(record['query'], record['query_type']) for record in custom_updates if record['query_type']]
elif table_name:
queries = [("update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % {

View File

@ -33,5 +33,39 @@
<p>Note: If you do not expect this, you can safely ignore this email.</p>]]></field>
</record>
<!-- Email template for new users -->
<record id="set_password_email" model="email.template">
<field name="name">OpenERP Enterprise Connection</field>
<field name="model_id" ref="base.model_res_users"/>
<field name="email_from"><![CDATA[${object.company_id.name} <${object.company_id.email}>]]></field>
<field name="email_to">${object.email}</field>
<field name="subject"><![CDATA[${object.company_id.name} invitation to connect on OpenERP]]></field>
<field name="body_html">
<![CDATA[
<p>
${object.name},
</p>
<p>
You have been invited to connect to "${object.company_id.name}" in order to get access to your documents in OpenERP.
</p>
<p>
To accept the invitation, click on the following link:
</p>
<ul>
<li><a href="${object.signup_url}">Accept invitation to "${object.company_id.name}"</a></li>
</ul>
<p>
Thanks,
</p>
<pre>
--
${object.company_id.name or ''}
${object.company_id.email or ''}
${object.company_id.phone or ''}
</pre>
]]>
</field>
</record>
</data>
</openerp>

View File

@ -246,8 +246,18 @@ class res_users(osv.Model):
partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)]
res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=now(days=+1), context=context)
if not context:
context = {}
# send email to users with their signup url
template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email')
template = False
if context.get('create_user'):
try:
template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'set_password_email')
except ValueError:
pass
if not bool(template):
template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email')
mail_obj = self.pool.get('mail.mail')
assert template._name == 'email.template'
for user in self.browse(cr, uid, ids, context):
@ -274,5 +284,6 @@ class res_users(osv.Model):
user_id = super(res_users, self).create(cr, uid, values, context=context)
user = self.browse(cr, uid, user_id, context=context)
if context and context.get('reset_password') and user.email:
user.action_reset_password()
ctx = dict(context, create_user=True)
self.action_reset_password(cr, uid, [user.id], context=ctx)
return user_id

View File

@ -164,7 +164,10 @@
background: #efc9cb;
}
.oe_import .select2-results {
font-size: 12px;
/* Field dropdown */
.oe_import_selector {
font-size: 10px;
/* copied from base.sass:~148 */
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
}

View File

@ -134,6 +134,7 @@ openerp.base_import = function (instance) {
start: function () {
var self = this;
this.setup_encoding_picker();
this.setup_separator_picker();
return $.when(
this._super(),
@ -164,6 +165,26 @@ openerp.base_import = function (instance) {
}
}).select2('val', 'utf-8');
},
setup_separator_picker: function () {
this.$('input.oe_import_separator').select2({
width: '160px',
query: function (q) {
var suggestions = [
{id: ',', text: _t("Comma")},
{id: ';', text: _t("Semicolon")},
{id: '\t', text: _t("Tab")},
{id: ' ', text: _t("Space")}
];
if (q.term) {
suggestions.unshift({id: q.term, text: q.term});
}
q.callback({results: suggestions});
},
initSelection: function (e, c) {
return c({id: ',', text: _t("Comma")});
},
});
},
import_options: function () {
var self = this;
@ -336,8 +357,18 @@ openerp.base_import = function (instance) {
var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
return $(el).select2('val') || false;
}).get();
return this.Import.call(
'do', [this.id, fields, this.import_options()], options);
return this.Import.call('do', [this.id, fields, this.import_options()], options)
.then(undefined, function (error, event) {
// In case of unexpected exception, convert
// "JSON-RPC error" to an import failure, and
// prevent default handling (warning dialog)
if (event) { event.preventDefault(); }
return $.when([{
type: 'error',
record: false,
message: error.data.fault_code,
}]);
}) ;
},
onvalidate: function () {
return this.call_import({ dryrun: true })

View File

@ -36,6 +36,7 @@ You can track your suppliers, customers and other contacts.
'data': [
'contacts_view.xml',
],
'images': ['images/contacts.jpeg'],
'installable': True,
'application': True,
'auto_install': False,

View File

@ -54,6 +54,7 @@ Dashboard for CRM will include:
'base_status',
'process',
'mail',
'email_template',
'base_calendar',
'resource',
'board',
@ -116,6 +117,6 @@ Dashboard for CRM will include:
'installable': True,
'application': True,
'auto_install': False,
'images': ['images/sale_crm_crm_dashboard.png', 'images/crm_dashboard.jpeg','images/leads.jpeg','images/meetings.jpeg','images/opportunities.jpeg','images/outbound_calls.jpeg','images/stages.jpeg'],
'images': ['images/crm_dashboard.png', 'images/customers.png','images/leads.png','images/opportunities_kanban.png','images/opportunities_form.png','images/opportunities_calendar.png','images/opportunities_graph.png','images/logged_calls.png','images/scheduled_calls.png','images/stages.png'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -36,7 +36,6 @@ CRM_LEAD_FIELDS_TO_MERGE = ['name',
'company_id',
'country_id',
'section_id',
'stage_id',
'state_id',
'type_id',
'user_id',
@ -472,6 +471,15 @@ class crm_lead(base_stage, format_address, osv.osv):
return 'lead'
def _merge_get_result_stage(self, cr, uid, opps, context=None):
stage = None
for opp in opps:
if not stage:
stage = opp.stage_id.id
if opp.type == 'opportunity':
return opp.stage_id.id
return stage
def _merge_data(self, cr, uid, ids, oldest, fields, context=None):
"""
Prepare lead/opp data into a dictionary for merging. Different types
@ -488,10 +496,8 @@ class crm_lead(base_stage, format_address, osv.osv):
opportunities = self.browse(cr, uid, ids, context=context)
def _get_first_not_null(attr):
if hasattr(oldest, attr):
return getattr(oldest, attr)
for opp in opportunities:
if hasattr(opp, attr):
if hasattr(opp, attr) and bool(getattr(opp, attr)):
return getattr(opp, attr)
return False
@ -520,7 +526,7 @@ class crm_lead(base_stage, format_address, osv.osv):
# Define the resulting type ('lead' or 'opportunity')
data['type'] = self._merge_get_result_type(cr, uid, opportunities, context)
data['stage_id'] = self._merge_get_result_stage(cr, uid, opportunities, context)
return data
def _merge_find_oldest(self, cr, uid, ids, context=None):
@ -533,9 +539,6 @@ class crm_lead(base_stage, format_address, osv.osv):
if context is None:
context = {}
if context.get('convert'):
ids = list(set(ids) - set(context.get('lead_ids', [])))
# Search opportunities order by create date
opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date', context=context)
oldest_opp_id = opportunity_ids[0]
@ -643,19 +646,11 @@ class crm_lead(base_stage, format_address, osv.osv):
if len(ids) <= 1:
raise osv.except_osv(_('Warning!'),_('Please select more than one element (lead or opportunity) from the list view.'))
lead_ids = context.get('lead_ids', [])
ctx_opportunities = self.browse(cr, uid, lead_ids, context=context)
opportunities = self.browse(cr, uid, ids, context=context)
opportunities_list = list(set(opportunities) - set(ctx_opportunities))
ids.sort()
oldest = self._merge_find_oldest(cr, uid, ids, context=context)
if ctx_opportunities:
first_opportunity = ctx_opportunities[0]
tail_opportunities = opportunities_list + ctx_opportunities[1:]
else:
first_opportunity = opportunities_list[0]
tail_opportunities = opportunities_list[1:]
opportunities_rest = self.browse(cr, uid, list(set(ids) - set([oldest.id])), context=context)
first_opportunity = oldest
tail_opportunities = opportunities_rest
merged_data = self._merge_data(cr, uid, ids, oldest, CRM_LEAD_FIELDS_TO_MERGE, context=context)
@ -664,14 +659,14 @@ class crm_lead(base_stage, format_address, osv.osv):
self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context)
# Merge notifications about loss of information
opportunities = [oldest]
opportunities.extend(opportunities_rest)
self._merge_notify(cr, uid, first_opportunity, opportunities, context=context)
# Write merged data into first opportunity
self.write(cr, uid, [first_opportunity.id], merged_data, context=context)
# Delete tail opportunities
self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context)
# Open first opportunity
self.case_open(cr, uid, [first_opportunity.id])
return first_opportunity.id
def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None):
@ -716,7 +711,7 @@ class crm_lead(base_stage, format_address, osv.osv):
def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
partner = self.pool.get('res.partner')
vals = { 'name': name,
vals = {'name': name,
'user_id': lead.user_id.id,
'comment': lead.description,
'section_id': lead.section_id.id or False,
@ -736,11 +731,11 @@ class crm_lead(base_stage, format_address, osv.osv):
'is_company': is_company,
'type': 'contact'
}
partner = partner.create(cr, uid,vals, context)
partner = partner.create(cr, uid, vals, context=context)
return partner
def _create_lead_partner(self, cr, uid, lead, context=None):
partner_id = False
partner_id = False
if lead.partner_name and lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
@ -748,8 +743,14 @@ class crm_lead(base_stage, format_address, osv.osv):
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
elif not lead.partner_name and lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context)
elif lead.email_from and self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]:
contact_name = self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]
partner_id = self._lead_create_contact(cr, uid, lead, contact_name, False, context=context)
else:
partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context)
raise osv.except_osv(
_('Warning!'),
_('No customer name defined. Please fill one of the following fields: Company Name, Contact Name or Email ("Name <email@address>")')
)
return partner_id
def _lead_set_partner(self, cr, uid, lead, partner_id, context=None):
@ -763,7 +764,7 @@ class crm_lead(base_stage, format_address, osv.osv):
res = False
res_partner = self.pool.get('res.partner')
if partner_id:
res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False})
res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False})
contact_id = res_partner.address_get(cr, uid, [partner_id])['default']
res = lead.write({'partner_id': partner_id}, context=context)
message = _("<b>Partner</b> set to <em>%s</em>." % (lead.partner_id.name))
@ -872,8 +873,8 @@ class crm_lead(base_stage, format_address, osv.osv):
'res_id': int(opportunity_id),
'view_id': False,
'views': [(form_view or False, 'form'),
(tree_view or False, 'tree'),
(False, 'calendar'), (False, 'graph')],
(tree_view or False, 'tree'),
(False, 'calendar'), (False, 'graph')],
'type': 'ir.actions.act_window',
}
@ -980,8 +981,11 @@ class crm_lead(base_stage, format_address, osv.osv):
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'partner_id': msg.get('author_id', False),
'user_id': False,
}
if msg.get('author_id'):
defaults.update(self.on_change_partner(cr, uid, None, msg.get('author_id'), context=context)['value'])
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
defaults['priority'] = msg.get('priority')
defaults.update(custom_values)

View File

@ -171,11 +171,13 @@
<record id="mt_lead_stage" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<field name="res_model">crm.lead</field>
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<record id="mt_lead_won" model="mail.message.subtype">
<field name="name">Opportunity Won</field>
<field name="res_model">crm.lead</field>
<field name="default" eval="False"/>
<field name="description">Opportunity won</field>
</record>
<record id="mt_lead_lost" model="mail.message.subtype">

View File

@ -168,7 +168,7 @@
</group>
</group>
<notebook colspan="4">
<page string="Notes">
<page string="Internal Notes">
<field name="description"/>
</page>
<page string="Extra Info">
@ -219,12 +219,14 @@
<field name="date_deadline" invisible="1"/>
<field name="create_date" groups="base.group_no_one"/>
<field name="name"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)"/>
<field name="email_from"/>
<field name="phone"/>
<field name="stage_id"/>
<field name="user_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="section_id" invisible="context.get('invisible_section', True)"/>
<field name="state" invisible="1"/>
<field name="type_id" invisible="1"/>
@ -235,6 +237,7 @@
</field>
</record>
<!-- CRM Lead Calendar View -->
<record model="ir.ui.view" id="crm_case_calendar_view_leads">
<field name="name">CRM - Leads Calendar</field>
@ -322,35 +325,39 @@
<search string="Search Leads">
<field name="name" string="Lead / Customer" filter_domain="['|','|',('partner_name','ilike',self),('email_from','ilike',self),('name','ilike',self)]"/>
<field name="categ_ids" string="Category" filter_domain="[('categ_ids','ilike',self)]"/>
<field name="create_date"/>
<filter icon="terp-mail-message-new" string="Unread Messages" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
<separator/>
<filter icon="terp-check" string="New" name="new" help="New Leads" domain="[('state','=','draft')]"/>
<filter icon="terp-camera_test" string="In Progress" name="open" domain="[('state','=','open')]"/>
<separator/>
<filter string="Unassigned Leads" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Leads"/>
<separator/>
<filter string="Leads Assigned to Me or My Team(s)" icon="terp-personal+" context="{'invisible_section': False}"
domain="['|', ('section_id.user_id','=',uid), ('section_id.member_ids', 'in', [uid])]"
help="Leads that are assigned to one of the sale teams I manage, or to me"/>
<field name="section_id" context="{'invisible_section': False, 'default_section_id': self}"/>
<field name="user_id"/>
<field name="section_id" context="{'invisible_section': False}"/>
<field name="create_date"/>
<field name="country_id" context="{'invisible_country': False}"/>
<separator/>
<filter string="Open" name="open" domain="[('state','!=','cancel')]" help="Open Leads"/>
<filter string="Dead" name="dead" domain="[('state','=','cancel')]"/>
<filter string="Unassigned" domain="[('user_id','=', False)]" help="No salesperson"/>
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]" help="Unread messages"/>
<filter string="Assigned to Me"
domain="[('user_id','=',uid)]" context="{'invisible_section': False}"
help="Leads that are assigned to me"/>
<filter string="Assigned to My Team(s)"
domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
help="Leads that are assigned to any sales teams I am member of"/>
<separator/>
<group expand="0" string="Group By...">
<filter string="Salesperson" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Team" icon="terp-personal+" domain="[]" context="{'group_by':'section_id'}"/>
<filter string="Referrer" icon="terp-personal" domain="[]" context="{'group_by':'referred'}"/>
<filter string="Campaign" icon="terp-gtk-jump-to-rtl" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" icon="terp-call-start" domain="[]" context="{'group_by':'channel_id'}"/>
<separator orientation="vertical"/>
<filter string="Stage" icon="terp-stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Creation" help="Create date" icon="terp-go-month" domain="[]" context="{'group_by':'create_date'}" groups="base.group_no_one"/>
<filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Team" domain="[]" context="{'group_by':'section_id'}"/>
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Country" domain="[]" context="{'group_by':'country_id'}"/>
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Referrer" domain="[]" context="{'group_by':'referred'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter string="Creation" domain="[]" context="{'group_by':'create_date'}" groups="base.group_no_one"/>
</group>
<group string="Display">
<filter string="Show Countries" icon="terp-personal+" context="{'invisible_country': False}" help="Show Countries"/>
<filter string="Show Sales Team" icon="terp-personal+" context="{'invisible_section': False}" domain="[]" help="Show Sales Team"/>
<filter string="Show Countries" context="{'invisible_country': False}" help="Show Countries"/>
<filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team"/>
</group>
</search>
</search>
</field>
</record>
@ -517,6 +524,7 @@
<field name="probability" avg="Avg. of Probability"/>
<field name="section_id" invisible="context.get('invisible_section', True)"/>
<field name="user_id"/>
<field name="referred" invisible="1"/>
<field name="priority" invisible="1"/>
<field name="state" groups="base.group_no_one"/>
<field name="message_unread" invisible="1"/>
@ -531,35 +539,41 @@
<field name="model">crm.lead</field>
<field name="arch" type="xml">
<search string="Search Opportunities">
<field name="name" string="Opportunity"
filter_domain="['|','|','|',('partner_id','ilike',self),('partner_name','ilike',self),('email_from','ilike',self),('name', 'ilike', self)]"/>
<field name="name" string="Opportunity" filter_domain="['|','|','|',('partner_id','ilike',self),('partner_name','ilike',self),('email_from','ilike',self),('name', 'ilike', self)]"/>
<field name="categ_ids" string="Category" filter_domain="[('categ_ids','ilike', self)]"/>
<filter icon="terp-mail-message-new" string="Unread Messages" help="Unread messages" name="message_unread" domain="[('message_unread','=',True)]"/>
<separator/>
<filter icon="terp-check" string="New" help="New Opportunities" name="new" domain="[('state','=','draft')]"/>
<filter icon="terp-camera_test" string="In Progress" help="Open Opportunities" name="open" domain="[('state','=','open')]"/>
<separator/>
<filter string="Unassigned Opportunities" icon="terp-personal-" domain="[('user_id','=', False)]" help="Unassigned Opportunities"/>
<separator/>
<filter string="Opportunities Assigned to Me or My Team(s)" icon="terp-personal+"
domain="['|', ('section_id.user_id','=',uid), ('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
help="Opportunities that are assigned to either me or one of the sale teams I manage"/>
<field name="user_id"/>
<field name="section_id" context="{'invisible_section': False, 'default_section_id': self}"/>
<field name="user_id"/>
<field name="partner_id"/>
<separator/>
<filter string="New" name="new" domain="[('state','=','draft')]" help="New Opportunities"/>
<filter string="In Progress" name="open" domain="[('state','=','open')]" help="Open Opportunities"/>
<filter string="Won" name="won" domain="[('state','=','done')]"/>
<filter string="Lost" name="lost" domain="[('state','=','cancel')]"/>
<filter string="Unassigned" domain="[('user_id','=', False)]" help="No salesperson"/>
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]" help="Unread messages"/>
<filter string="Assigned to Me"
domain="[('user_id','=',uid)]" context="{'invisible_section': False}"
help="Opportunities that are assigned to me"/>
<filter string="Assigned to My Team(s)"
domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
help="Opportunities that are assigned to any sales teams I am member of"/>
<separator/>
<group expand="0" string="Group By..." colspan="16">
<filter string="Salesperson" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Team" help="Sales Team" icon="terp-personal+" domain="[]" context="{'group_by':'section_id'}"/>
<filter string="Customer" help="Partner" icon="terp-personal+" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Stage" icon="terp-stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Priority" icon="terp-rating-rated" domain="[]" context="{'group_by':'priority'}"/>
<filter string="Campaign" icon="terp-gtk-jump-to-rtl" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" icon="terp-call-start" domain="[]" context="{'group_by':'channel_id'}"/>
<filter string="Creation" icon="terp-go-month" domain="[]" context="{'group_by':'create_date'}" groups="base.group_no_one"/>
<filter string="Exp.Closing" icon="terp-go-month" help="Expected Closing" domain="[]" context="{'group_by':'date_deadline'}"/>
<filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Team" domain="[]" context="{'group_by':'section_id'}"/>
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Country" domain="[]" context="{'group_by':'country_id'}"/>
<filter string="Priority" domain="[]" context="{'group_by':'priority'}"/>
<filter string="Expected Closing" domain="[]" context="{'group_by':'date_deadline'}"/>
<filter string="Referrer" domain="[]" context="{'group_by':'referred'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter string="Creation" domain="[]" context="{'group_by':'create_date'}" groups="base.group_no_one"/>
</group>
<group string="Display">
<filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team"/>
<filter string="Show Countries" context="{'invisible_country': False}" help="Show Countries"/>
</group>
</search>
</field>

View File

@ -68,24 +68,4 @@ class calendar_attendee(osv.osv):
relation="crm.case.categ", multi='categ_id'),
}
class res_users(osv.osv):
_name = 'res.users'
_inherit = 'res.users'
def create(self, cr, uid, data, context=None):
user_id = super(res_users, self).create(cr, uid, data, context=context)
# add shortcut unless 'noshortcut' is True in context
if not(context and context.get('noshortcut', False)):
data_obj = self.pool.get('ir.model.data')
try:
data_id = data_obj._get_id(cr, uid, 'crm', 'ir_ui_view_sc_calendar0')
view_id = data_obj.browse(cr, uid, data_id, context=context).res_id
self.pool.get('ir.ui.view_sc').copy(cr, uid, view_id, default = {
'user_id': user_id}, context=context)
except:
# Tolerate a missing shortcut. See product/product.py for similar code.
_logger.debug('Skipped meetings shortcut for user "%s".', data.get('name','<new'))
return user_id
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -4,31 +4,37 @@
!record {model: crm.lead, id: test_crm_lead_01}:
type: 'lead'
name: 'Test lead 1'
email_from: 'Raoul Grosbedon <raoul@grosbedon.fr>'
stage_id: stage_lead1
-
!record {model: crm.lead, id: test_crm_lead_02}:
type: 'lead'
name: 'Test lead 2'
email_from: 'Raoul Grosbedon <raoul@grosbedon.fr>'
stage_id: stage_lead1
-
!record {model: crm.lead, id: test_crm_lead_03}:
type: 'lead'
name: 'Test lead 3'
email_from: 'Raoul Grosbedon <raoul@grosbedon.fr>'
stage_id: stage_lead1
-
!record {model: crm.lead, id: test_crm_lead_04}:
type: 'lead'
name: 'Test lead 4'
contact_name: 'Fabrice Lepoilu'
stage_id: stage_lead1
-
!record {model: crm.lead, id: test_crm_lead_05}:
type: 'lead'
name: 'Test lead 5'
contact_name: 'Fabrice Lepoilu'
stage_id: stage_lead1
-
!record {model: crm.lead, id: test_crm_lead_06}:
type: 'lead'
name: 'Test lead 6'
partner_name: 'Agrolait SuperSeed SA'
stage_id: stage_lead1
-
!record {model: res.users, id: test_res_user_01}:

View File

@ -34,7 +34,7 @@ class crm_lead2opportunity_partner(osv.osv_memory):
('convert', 'Convert to opportunity'),
('merge', 'Merge with existing opportunities')
], 'Conversion Action', required=True),
'opportunity_ids': fields.many2many('crm.lead', string='Opportunities', domain=[('type', '=', 'opportunity')]),
'opportunity_ids': fields.many2many('crm.lead', string='Opportunities'),
}
def default_get(self, cr, uid, fields, context=None):
@ -46,42 +46,34 @@ class crm_lead2opportunity_partner(osv.osv_memory):
lead_obj = self.pool.get('crm.lead')
res = super(crm_lead2opportunity_partner, self).default_get(cr, uid, fields, context=context)
opportunities = res.get('opportunity_ids') or []
partner_id = False
email = False
for lead in lead_obj.browse(cr, uid, opportunities, context=context):
partner_id = lead.partner_id and lead.partner_id.id or False
if context.get('active_id'):
tomerge = set([int(context['active_id'])])
email = False
partner_id = res.get('partner_id')
lead = lead_obj.browse(cr, uid, int(context['active_id']), context=context)
#TOFIX: use mail.mail_message.to_mail
email = re.findall(r'([^ ,<@]+@[^> ,]+)', lead.email_from or '')
email = map(lambda x: "'" + x + "'", email)
if not partner_id and res.get('partner_id'):
partner_id = res.get('partner_id')
ids = []
if partner_id:
# Search for opportunities that have the same partner and that arent done or cancelled
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), ('type', '=', 'opportunity'), '!', ('state', 'in', ['done', 'cancel'])])
if ids:
opportunities.append(ids[0])
if not partner_id:
if partner_id:
# Search for opportunities that have the same partner and that arent done or cancelled
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id)])
for id in ids:
tomerge.add(id)
if email:
# Find email of existing opportunity matching the email_from of the lead
cr.execute("""select id from crm_lead where type='opportunity' and
substring(email_from from '([^ ,<@]+@[^> ,]+)') in (%s)""" % (','.join(email)))
ids = map(lambda x:x[0], cr.fetchall())
if ids:
opportunities.append(ids[0])
ids = lead_obj.search(cr, uid, [('email_from', 'ilike', email[0])])
for id in ids:
tomerge.add(id)
if 'action' in fields:
res.update({'action' : partner_id and 'exist' or 'create'})
if 'partner_id' in fields:
res.update({'partner_id' : partner_id})
if 'name' in fields:
res.update({'name' : ids and 'merge' or 'convert'})
if 'opportunity_ids' in fields:
res.update({'opportunity_ids': opportunities})
if 'action' in fields:
res.update({'action' : partner_id and 'exist' or 'create'})
if 'partner_id' in fields:
res.update({'partner_id' : partner_id})
if 'name' in fields:
res.update({'name' : len(tomerge) >= 2 and 'merge' or 'convert'})
if 'opportunity_ids' in fields and len(tomerge) >= 2:
res.update({'opportunity_ids': list(tomerge)})
return res
@ -115,38 +107,44 @@ class crm_lead2opportunity_partner(osv.osv_memory):
lead.allocate_salesman(cr, uid, lead_ids, user_ids, team_id=team_id, context=context)
return res
def _merge_opportunity(self, cr, uid, ids, opportunity_ids, action='merge', context=None):
if context is None:
context = {}
res = False
# Expected: all newly-converted leads (active_ids) will be merged with the opportunity(ies)
# that have been selected in the 'opportunity_ids' m2m, with all these records
# merged into the first opportunity (and the rest deleted)
opportunity_ids = [o.id for o in opportunity_ids]
lead_ids = context.get('active_ids', [])
if action == 'merge' and lead_ids and opportunity_ids:
# Add the leads in the to-merge list, next to other opps
# (the fact that they're passed in context['lead_ids'] means that
# they cannot be selected to contain the result of the merge.
opportunity_ids.extend(lead_ids)
context.update({'lead_ids': lead_ids, "convert" : True})
res = self.pool.get('crm.lead').merge_opportunity(cr, uid, opportunity_ids, context=context)
return res
def action_apply(self, cr, uid, ids, context=None):
"""
Convert lead to opportunity or merge lead and opportunity and open
the freshly created opportunity view.
"""
if context is None:
context = {}
w = self.browse(cr, uid, ids, context=context)[0]
opp_ids = [o.id for o in w.opportunity_ids]
if w.name == 'merge':
lead_id = self.pool.get('crm.lead').merge_opportunity(cr, uid, opp_ids, context=context)
lead_ids = [lead_id]
lead = self.pool.get('crm.lead').read(cr, uid, lead_id, ['type'], context=context)
if lead['type'] == "lead":
context.update({'active_ids': lead_ids})
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context)
else:
lead_ids = context.get('active_ids', [])
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context)
return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context)
def _create_partner(self, cr, uid, ids, context=None):
"""
Create partner based on action.
:return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
"""
#TODO this method in only called by crm_lead2opportunity_partner
#wizard and would probably diserve to be refactored or at least
#moved to a better place
if context is None:
context = {}
lead = self.pool.get('crm.lead')
lead_ids = context.get('active_ids', [])
data = self.browse(cr, uid, ids, context=context)[0]
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context)
self._merge_opportunity(cr, uid, ids, data.opportunity_ids, data.name, context=context)
return lead.redirect_opportunity_view(cr, uid, lead_ids[0], context=context)
partner_id = data.partner_id and data.partner_id.id or False
return lead.handle_partner_assignation(cr, uid, lead_ids, data.action, partner_id, context=context)
class crm_lead2opportunity_mass_convert(osv.osv_memory):
_name = 'crm.lead2opportunity.partner.mass'

View File

@ -9,11 +9,19 @@
<form string="Convert to Opportunity" version="7.0">
<group name="name">
<field name="name" class="oe_inline"/>
<field name="opportunity_ids" attrs="{'invisible': [('name', '!=', 'merge')]}">
</group>
<group string="Opportunities">
<field name="opportunity_ids" attrs="{'invisible': [('name', '!=', 'merge')]}" nolabel="1">
<tree>
<field name="create_date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)"/>
<field name="email_from"/>
<field name="phone"/>
<field name="stage_id"/>
<field name="user_id" invisible="1"/>
<field name="section_id"/>
</tree>
</field>
@ -51,9 +59,15 @@
<group string="Select Opportunities" attrs="{'invisible': [('name', '!=', 'merge')]}">
<field name="opportunity_ids" colspan="4" nolabel="1" attrs="{'invisible': [('name', '=', 'convert')]}">
<tree>
<field name="create_date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="type"/>
<field name="contact_name"/>
<field name="country_id" invisible="context.get('invisible_country', True)"/>
<field name="email_from"/>
<field name="phone"/>
<field name="stage_id"/>
<field name="user_id" invisible="1"/>
<field name="section_id"/>
</tree>
</field>

View File

@ -11,9 +11,14 @@
<separator string="Select Leads/Opportunities"/>
<field name="opportunity_ids">
<tree>
<field name="create_date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="user_id"/>
<field name="type"/>
<field name="contact_name"/>
<field name="email_from"/>
<field name="phone"/>
<field name="stage_id"/>
<field name="user_id" invisible="1"/>
<field name="section_id"/>
</tree>
</field>

View File

@ -96,20 +96,4 @@ class crm_partner_binding(osv.osv_memory):
return res
def _create_partner(self, cr, uid, ids, context=None):
"""
Create partner based on action.
:return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
"""
#TODO this method in only called by crm_lead2opportunity_partner
#wizard and would probably diserve to be refactored or at least
#moved to a better place
if context is None:
context = {}
lead = self.pool.get('crm.lead')
lead_ids = context.get('active_ids', [])
data = self.browse(cr, uid, ids, context=context)[0]
partner_id = data.partner_id and data.partner_id.id or False
return lead.handle_partner_assignation(cr, uid, lead_ids, data.action, partner_id, context=context)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -195,6 +195,7 @@ class crm_claim(base_stage, osv.osv):
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'partner_id': msg.get('author_id', False),
}
if msg.get('priority'):
defaults['priority'] = msg.get('priority')

View File

@ -106,6 +106,7 @@ class crm_helpdesk(base_state, base_stage, osv.osv):
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
'partner_id': msg.get('author_id', False),
}
defaults.update(custom_values)
return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=defaults, context=context)

View File

@ -57,6 +57,6 @@ Key Features
'installable': True,
'application': True,
'auto_install': False,
'images': ['images/1_event_type_list.jpeg','images/2_events.jpeg','images/3_registrations.jpeg'],
'images': ['images/1_event_type_list.jpeg','images/2_events.jpeg','images/3_registrations.jpeg','images/events_kanban.jpeg'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -54,6 +54,7 @@ Main Features
'fleet_data.xml',
'fleet_board_view.xml',
],
'images': ['images/costs_analysis.jpeg','images/indicative_costs_analysis.jpeg','images/vehicles.jpeg','images/vehicles_contracts.jpeg','images/vehicles_fuel.jpeg','images/vehicles_odometer.jpeg','images/vehicles_services.jpeg'],
'update_xml' : ['security/fleet_security.xml','security/ir.model.access.csv'],
'demo': ['fleet_demo.xml'],

View File

@ -29,13 +29,40 @@ _logger = logging.getLogger(__name__)
try:
import gdata.docs.data
import gdata.docs.client
from gdata.client import RequestError
from gdata.docs.service import DOCUMENT_LABEL
import gdata.auth
from gdata.docs.data import Resource
# API breakage madness in the gdata API - those guys are insane.
try:
# gdata 2.0.15+
gdata.docs.client.DocsClient.copy_resource
except AttributeError:
# gdata 2.0.14- : copy_resource() was copy()
gdata.docs.client.DocsClient.copy_resource = gdata.docs.client.DocsClient.copy
try:
# gdata 2.0.16+
gdata.docs.client.DocsClient.get_resource_by_id
except AttributeError:
try:
# gdata 2.0.15+
gdata.docs.client.DocsClient.get_resource_by_self_link
def get_resource_by_id_2_0_16(self, resource_id, **kwargs):
return self.GetResourceBySelfLink(
gdata.docs.client.RESOURCE_FEED_URI + ('/%s' % resource_id), **kwargs)
gdata.docs.client.DocsClient.get_resource_by_id = get_resource_by_id_2_0_16
except AttributeError:
# gdata 2.0.14- : alias get_resource_by_id()
gdata.docs.client.DocsClient.get_resource_by_id = gdata.docs.client.DocsClient.get_doc
try:
import atom.http_interface
_logger.info('GData lib version `%s` detected' % atom.http_interface.USER_AGENT)
except (ImportError, AttributeError):
_logger.debug('GData lib version could not be detected', exc_info=True)
except ImportError:
_logger.warning("Please install latest gdata-python-client from http://code.google.com/p/gdata-python-client/downloads/list")
class google_docs_ir_attachment(osv.osv):
_inherit = 'ir.attachment'
@ -49,11 +76,12 @@ class google_docs_ir_attachment(osv.osv):
#get gmail password and login. We use default_get() instead of a create() followed by a read() on the
# google.login object, because it is easier. The keys 'user' and 'password' ahve to be passed in the dict
# but the values will be replaced by the user gmail password and login.
user_config = google_pool.default_get( cr, uid, {'user' : '' , 'password' : ''}, context=context)
user_config = google_pool.default_get(cr, uid, {'user' : '' , 'password' : ''}, context=context)
#login gmail account
client = google_pool.google_login( user_config['user'], user_config['password'], type='docs_client', context=context)
client = google_pool.google_login(user_config['user'], user_config['password'], type='docs_client', context=context)
if not client:
raise osv.except_osv( _('Google Docs Error!'), _("Check your google configuration in Users/Users/Synchronization tab."))
raise osv.except_osv(_('Google Docs Error!'), _("Check your google configuration in Users/Users/Synchronization tab."))
_logger.info('Logged into google docs as %s', user_config['user'])
return client
def create_empty_google_doc(self, cr, uid, res_model, res_id, context=None):
@ -94,9 +122,9 @@ class google_docs_ir_attachment(osv.osv):
client = self._auth(cr, uid)
# fetch and copy the original document
try:
doc = client.GetDoc(gdoc_template_id)
doc = client.get_resource_by_id(gdoc_template_id)
#copy the document you choose in the configuration
copy_resource = client.copy(doc, name_gdocs)
copy_resource = client.copy_resource(doc, name_gdocs)
except:
raise osv.except_osv(_('Google Docs Error!'), _("Your resource id is not correct. You can find the id in the google docs URL."))
# create an ir.attachment

View File

@ -27,7 +27,7 @@
'sequence': 31,
'website': 'http://www.openerp.com',
'summary': 'Periodical Evaluations, Appraisals, Surveys',
'images': ['images/hr_evaluation_analysis.jpeg','images/hr_evaluation.jpeg'],
'images': ['images/hr_evaluation_analysis.jpeg','images/hr_evaluation.jpeg','images/hr_interview_requests.jpeg'],
'depends': ['hr','base_calendar','survey'],
'description': """
Periodical Employees evaluation and appraisals

View File

@ -353,6 +353,7 @@ class hr_applicant(base_stage, osv.Model):
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
'partner_id': msg.get('author_id', False),
}
if msg.get('priority'):
defaults['priority'] = msg.get('priority')

View File

@ -470,11 +470,13 @@
<record id="mt_stage_changed" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<field name="res_model">hr.applicant</field>
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<record id="mt_applicant_hired" model="mail.message.subtype">
<field name="name">Applicant Hired</field>
<field name="res_model">hr.applicant</field>
<field name="default" eval="False"/>
<field name="description">Applicant hired</field>
</record>
<record id="mt_applicant_refused" model="mail.message.subtype">

View File

@ -45,7 +45,7 @@ The validation can be configured in the company:
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'images': ['images/hr_my_timesheet.jpeg','images/hr_timesheet_analysis.jpeg','images/hr_timesheet_sheet_analysis.jpeg','images/hr_timesheets.jpeg'],
'images': ['images/hr_my_current_timesheet.jpeg','images/hr_timesheet_analysis.jpeg','images/hr_timesheet_sheet_analysis.jpeg','images/hr_timesheet_activity.jpeg'],
'depends': ['hr_timesheet', 'hr_timesheet_invoice', 'process'],
'data': [
'security/ir.model.access.csv',

View File

@ -44,6 +44,7 @@ If you want to save your employees' time and avoid them to always have coins in
'report/report_lunch_order_view.xml',
'security/ir.model.access.csv',],
'css':['static/src/css/lunch.css'],
'images': ['images/new_order.jpeg','images/lunch_account.jpeg','images/order_by_supplier_analysis.jpeg','images/alert.jpeg'],
'demo': ['lunch_demo.xml',],
'installable': True,
'application' : True,

View File

@ -39,7 +39,8 @@
<field name="user_id"/>
<filter name='is_payment' string="Payment" domain="[('state','=','payment')]"/>
<separator/>
<filter name='is_mine' string="My Account" domain="[('user_id','=',uid)]"/>
<filter name='is_mine_group' string="My Account grouped" domain="[('user_id','=',uid)]" context="{'group_by':'user_id'}"/>
<filter name="group_by_user" string="By User" context="{'group_by':'user_id'}"/>
</search>
</field>
</record>
@ -116,7 +117,7 @@
<field name="res_model">lunch.cashmove</field>
<field name="view_mode">tree</field>
<field name="search_view_id" ref="view_lunch_employee_payment_filter"/>
<field name="context">{"search_default_is_mine":1}</field>
<field name="context">{"search_default_is_mine_group":1}</field>
<field name="help" type="html">
<p>
Here you can see your cash moves.<br/>A cash moves can be either an expense or a payment.

View File

@ -71,16 +71,12 @@ Main Features
'installable': True,
'application': True,
'images': [
'images/customer_history.jpeg',
'images/inbox.jpeg',
'images/messages_form.jpeg',
'images/messages_list.jpeg',
'static/src/img/email_icong.png',
'static/src/img/_al.png',
'static/src/img/_pincky.png',
'static/src/img/groupdefault.png',
'static/src/img/attachment.png',
'static/src/img/checklist.png',
'static/src/img/formatting.png',
'images/email.jpeg',
'images/join_a_group.jpeg',
'images/share_a_message.jpeg',
],
'css': [
'static/src/css/mail.css',

View File

@ -21,6 +21,8 @@ should inherit from this class.
ClientAction (ir.actions.client)
++++++++++++++++++++++++++++++++
.. code-block:: xml
<record id="action_mail_inbox_feeds" model="ir.actions.client">
<field name="name">Inbox</field>
<field name="tag">mail.wall</field>
@ -36,6 +38,7 @@ ClientAction (ir.actions.client)
'mail_thread' widget for field on standard view. (default value like a thread for
record, view on flat mode, no reply, no read/unread)
'mail.widget' it's the root thread, used by 'mail.wall' and 'mail_thread'
- ``help`` : Text HTML to display if there are no message
- ``context`` : insert 'default_model' and 'default_res_id'
- ``params`` : options for the widget

View File

@ -85,9 +85,6 @@ class mail_notification(osv.Model):
if notification.read:
continue
partner = notification.partner_id
# Do not send an email to the writer
if partner.user_ids and partner.user_ids[0].id == uid:
continue
# Do not send to partners without email address defined
if not partner.email:
continue
@ -129,11 +126,20 @@ class mail_notification(osv.Model):
if signature:
body_html = tools.append_content_to_html(body_html, signature, plaintext=True, container_tag='div')
# email_from: partner-user alias or partner email or mail.message email_from
if msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0].alias_domain and msg.author_id.user_ids[0].alias_name:
email_from = '%s <%s@%s>' % (msg.author_id.name, msg.author_id.user_ids[0].alias_name, msg.author_id.user_ids[0].alias_domain)
elif msg.author_id:
email_from = '%s <%s>' % (msg.author_id.name, msg.author_id.email)
else:
email_from = msg.email_from
mail_values = {
'mail_message_id': msg.id,
'email_to': [],
'auto_delete': True,
'body_html': body_html,
'email_from': email_from,
'state': 'outgoing',
}
mail_values['email_to'] = ', '.join(mail_values['email_to'])

View File

@ -78,6 +78,13 @@ class mail_mail(osv.Model):
'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
}
def default_get(self, cr, uid, fields, context=None):
# protection for `default_type` values leaking from menu action context (e.g. for invoices)
# To remove when automatic context propagation is removed in web client
if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection:
context = dict(context, default_type = None)
return super(mail_mail, self).default_get(cr, uid, fields, context=context)
def create(self, cr, uid, values, context=None):
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True

View File

@ -243,7 +243,7 @@ class mail_thread(osv.AbstractModel):
# subscribe uid unless asked not to
if not context.get('mail_create_nosubscribe'):
self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context)
self.message_subscribe_from_parent(cr, uid, [thread_id], values.keys(), context=context)
self.message_auto_subscribe(cr, uid, [thread_id], values.keys(), context=context)
# automatic logging unless asked not to (mainly for various testing purpose)
if not context.get('mail_create_nolog'):
@ -261,7 +261,7 @@ class mail_thread(osv.AbstractModel):
# Perform write, update followers
result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
self.message_subscribe_from_parent(cr, uid, ids, values.keys(), context=context)
self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context)
# Perform the tracking
if tracked_fields:
@ -1069,7 +1069,24 @@ class mail_thread(osv.AbstractModel):
self.check_access_rights(cr, uid, 'write')
return self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
def message_subscribe_from_parent(self, cr, uid, ids, updated_fields, context=None):
def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=['user_id'], context=None):
""" Returns the list of relational fields linking to res.users that should
trigger an auto subscribe. The default list checks for the fields
- called 'user_id'
- linking to res.users
- with track_visibility set
In OpenERP V7, this is sufficent for all major addon such as opportunity,
project, issue, recruitment, sale.
Override this method if a custom behavior is needed about fields
that automatically subscribe users.
"""
user_field_lst = []
for name, column_info in self._all_columns.items():
if name in auto_follow_fields and name in updated_fields and getattr(column_info.column, 'track_visibility', False) and column_info.column._obj == 'res.users':
user_field_lst.append(name)
return user_field_lst
def message_auto_subscribe(self, cr, uid, ids, updated_fields, context=None):
"""
1. fetch project subtype related to task (parent_id.res_model = 'project.task')
2. for each project subtype: subscribe the follower to the task
@ -1077,13 +1094,16 @@ class mail_thread(osv.AbstractModel):
subtype_obj = self.pool.get('mail.message.subtype')
follower_obj = self.pool.get('mail.followers')
# fetch auto_follow_fields
user_field_lst = self._message_get_auto_subscribe_fields(cr, uid, updated_fields, context=context)
# fetch related record subtypes
related_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('parent_id.res_model', '=', self._name)], context=context)
subtypes = subtype_obj.browse(cr, uid, related_subtype_ids, context=context)
default_subtypes = [subtype for subtype in subtypes if subtype.res_model == False]
related_subtypes = [subtype for subtype in subtypes if subtype.res_model != False]
relation_fields = set([subtype.relation_field for subtype in subtypes if subtype.relation_field != False])
if not related_subtypes or not any(relation in updated_fields for relation in relation_fields):
if (not related_subtypes or not any(relation in updated_fields for relation in relation_fields)) and not user_field_lst:
return True
for record in self.browse(cr, uid, ids, context=context):
@ -1105,20 +1125,24 @@ class mail_thread(osv.AbstractModel):
for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
new_followers.setdefault(follower.partner_id.id, set()).add(subtype.parent_id.id)
if not parent_res_id or not parent_model:
continue
if parent_res_id and parent_model:
for subtype in default_subtypes:
follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
('res_model', '=', parent_model),
('res_id', '=', parent_res_id),
('subtype_ids', 'in', [subtype.id])
], context=context)
for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id)
for subtype in default_subtypes:
follower_ids = follower_obj.search(cr, SUPERUSER_ID, [
('res_model', '=', parent_model),
('res_id', '=', parent_res_id),
('subtype_ids', 'in', [subtype.id])
], context=context)
for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context):
new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id)
# add followers coming from res.users relational fields that are tracked
user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]:
new_followers.setdefault(partner_id, None)
for pid, subtypes in new_followers.items():
self.message_subscribe(cr, uid, [record.id], [pid], list(subtypes), context=context)
subtypes = list(subtypes) if subtypes is not None else None
self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
return True
#------------------------------------------------------

View File

@ -105,7 +105,7 @@ openerp.mail = function (session) {
// As it only looks at the extension it is quite approximative.
filetype: function(url){
url = url.filename || url;
var tokens = url.split('.');
var tokens = (url+'').split('.');
if(tokens.length <= 1){
return 'unknown';
}
@ -218,7 +218,7 @@ openerp.mail = function (session) {
this.author_id = datasets.author_id || false,
this.attachment_ids = datasets.attachment_ids || [],
this.partner_ids = datasets.partner_ids || [];
this._date = datasets.date;
this.date = datasets.date;
this.format_data();
@ -232,19 +232,20 @@ openerp.mail = function (session) {
else {
this.options.show_read = this.to_read;
this.options.show_unread = !this.to_read;
this.options.rerender = true;
this.options.toggle_read = true;
}
this.options.rerender = true;
this.options.toggle_read = true;
}
this.parent_thread = parent.messages != undefined ? parent : this.options.root_thread;
this.parent_thread = typeof parent.on_message_detroy == 'function' ? parent : this.options.root_thread;
this.thread = false;
},
/* Convert date, timerelative and avatar in displayable data. */
format_data: function () {
//formating and add some fields for render
if (this._date) {
this.timerelative = $.timeago(this._date+"Z");
this.date = this.date ? session.web.str_to_datetime(this.date) : false;
if (this.date && new Date().getTime()-this.date.getTime() < 7*24*60*60*1000) {
this.timerelative = $.timeago(this.date);
}
if (this.type == 'email' && (!this.author_id || !this.author_id[0])) {
this.avatar = ('/mail/static/src/img/email_icon.png');
@ -253,7 +254,7 @@ openerp.mail = function (session) {
} else {
this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
}
if (this.author_id) {
if (this.author_id && this.author_id[1]) {
var email = this.author_id[1].match(/(.*)<(.*@.*)>/);
if (!email) {
this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]);
@ -271,7 +272,7 @@ openerp.mail = function (session) {
var attach = this.attachment_ids[l];
if (!attach.formating) {
attach.url = mail.ChatterUtils.get_attachment_url(this.session, this.id, attach.id);
attach.filetype = mail.ChatterUtils.filetype(attach.filename);
attach.filetype = mail.ChatterUtils.filetype(attach.filename || attach.name);
attach.name = mail.ChatterUtils.breakword(attach.name || attach.filename);
attach.formating = true;
}
@ -1605,7 +1606,8 @@ openerp.mail = function (session) {
this.node.params = _.extend({
'display_indented_thread': -1,
'show_reply_button': false,
'show_read_unread_button': false,
'show_read_unread_button': true,
'read_action': 'unread',
'show_record_name': false,
'show_compact_message': 1,
}, this.node.params);

View File

@ -216,9 +216,12 @@ openerp_mail_followers = function(session, mail) {
display_subtypes:function (data) {
var self = this;
var subtype_list_ul = this.$('.oe_subtype_list');
subtype_list_ul.empty();
var records = data[this.view.datarecord.id || this.view.dataset.ids[0]].message_subtype_data;
var records = [];
var nb_subtype = 0;
subtype_list_ul.empty();
if (this.view.datarecord.id) {
records = data[this.view.datarecord.id].message_subtype_data;
}
_(records).each(function (record) {nb_subtype++;});
if (nb_subtype > 1) {
this.$('hr').show();

View File

@ -246,7 +246,7 @@
<a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[2]"/></a>
<span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-raw="widget.author_id[2]"/></span>
<span class='oe_subtle'></span>
<span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span>
<span t-att-title="widget.date"><t t-if="widget.timerelative" t-raw="widget.timerelative"/><t t-if="!widget.timerelative" t-raw="widget.date"/></span>
<span t-if="!widget.options.readonly" class='oe_subtle'></span>
<t t-if="!widget.options.readonly" t-call="mail.thread.message.vote"/>
</div>

View File

@ -52,9 +52,16 @@ class membership_invoice(osv.osv_memory):
'amount': data.member_price
}
invoice_list = partner_obj.create_membership_invoice(cr, uid, context.get('active_ids', []), datas=datas, context=context)
res = mod_obj.get_object_reference(cr, uid, 'account', 'view_account_invoice_filter')
try:
search_view_id = mod_obj.get_object_reference(cr, uid, 'account', 'view_account_invoice_filter')[1]
except ValueError:
search_view_id = False
try:
form_view_id = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')[1]
except ValueError:
form_view_id = False
return {
'domain': [('id', 'in', invoice_list)],
'name': 'Membership Invoices',
@ -62,7 +69,8 @@ class membership_invoice(osv.osv_memory):
'view_mode': 'tree,form',
'res_model': 'account.invoice',
'type': 'ir.actions.act_window',
'search_view_id': res and res[1] or False
'views': [(False, 'tree'), (form_view_id, 'form')],
'search_view_id': search_view_id,
}
membership_invoice()

View File

@ -28,7 +28,7 @@
'category': 'Manufacturing',
'sequence': 18,
'summary': 'Manufacturing Orders, Bill of Materials, Routing',
'images': ['images/bill_of_materials.jpeg', 'images/manufacturing_order.jpeg', 'images/planning_manufacturing_order.jpeg', 'images/production_analysis.jpeg', 'images/production_dashboard.jpeg','images/routings.jpeg','images/work_centers.jpeg'],
'images': ['images/bill_of_materials.jpeg', 'images/manufacturing_order.jpeg', 'images/planning_manufacturing_order.jpeg', 'images/manufacturing_analysis.jpeg', 'images/production_dashboard.jpeg','images/routings.jpeg','images/work_centers.jpeg'],
'depends': ['product','procurement', 'stock', 'resource', 'purchase','process'],
'description': """
Manage the Manufacturing process in OpenERP

View File

@ -57,6 +57,11 @@ Notes can be found in the 'Home' menu.
'css': [
'static/src/css/note.css',
],
'images': [
'images/note_kanban.jpeg',
'images/note.jpeg',
'images/categories_tree.jpeg'
],
'installable': True,
'application': True,
'auto_install': False,

View File

@ -29,7 +29,7 @@ class note_stage(osv.osv):
_columns = {
'name': fields.char('Stage Name', translate=True, required=True),
'sequence': fields.integer('Sequence', help="Used to order the note stages"),
'user_id': fields.many2one('res.users', 'Owner', help="Owner of the note stage.", required=True),
'user_id': fields.many2one('res.users', 'Owner', help="Owner of the note stage.", required=True, ondelete='cascade'),
'fold': fields.boolean('Folded by Default'),
}
_order = 'sequence asc'
@ -112,7 +112,7 @@ class note_note(osv.osv):
'date_done': fields.date('Date done'),
'color': fields.integer('Color Index'),
'tag_ids' : fields.many2many('note.tag','note_tags_rel','note_id','tag_id','Tags'),
'current_partner_id' : fields.function(_get_my_current_partner),
'current_partner_id' : fields.function(_get_my_current_partner, type="many2one", relation='res.partner', string="Owner"),
}
_defaults = {
'open' : 1,

View File

@ -24,7 +24,7 @@ class pad_common(osv.osv_memory):
# make sure pad server in the form of http://hostname
if not pad["server"]:
return ''
return pad
if not pad["server"].startswith('http'):
pad["server"] = 'http://' + pad["server"]
pad["server"] = pad["server"].rstrip('/')
@ -96,7 +96,7 @@ class pad_common(osv.osv_memory):
field = v.column
if hasattr(field,'pad_content_field'):
pad = self.pad_generate_url(cr, uid, context)
default[k] = pad['url']
default[k] = pad.get('url')
return super(pad_common, self).copy(cr, uid, id, default, context)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -48,7 +48,7 @@ Main Features
* Refund previous sales
""",
'author': 'OpenERP SA',
'images': ['images/cash_registers.jpeg', 'images/pos_analysis.jpeg','images/register_analysis.jpeg','images/sale_order_pos.jpeg','images/product_pos.jpeg'],
'images': ['images/pos_touch_screen.jpeg', 'images/pos_session.jpeg', 'images/pos_analysis.jpeg','images/sale_order_pos.jpeg','images/product_pos.jpeg'],
'depends': ['sale_stock'],
'data': [
'security/point_of_sale_security.xml',

View File

@ -853,8 +853,7 @@ class pos_order(osv.osv):
inv_line['price_unit'] = line.price_unit
inv_line['discount'] = line.discount
inv_line['name'] = inv_name
inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\
and [(6, 0, inv_line['invoice_line_tax_id'])] or []
inv_line['invoice_line_tax_id'] = [(6, 0, [x.id for x in line.product_id.taxes_id] )]
inv_line_ref.create(cr, uid, inv_line, context=context)
inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context)
wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr)
@ -1156,7 +1155,6 @@ class pos_order_line(osv.osv):
prod = self.pool.get('product.product').browse(cr, uid, product, context=context)
taxes = prod.taxes_id
price = price_unit * (1 - (discount or 0.0) / 100.0)
taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False)

View File

@ -233,7 +233,9 @@
font-style: italic;
cursor:pointer;
}
.point-of-sale .oe_pos_synch-notification.oe_inactive{
cursor: default;
}
.point-of-sale .oe_pos_synch-notification .oe_status_red{
display:inline-block;
cursor:pointer;
@ -542,7 +544,9 @@
background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
/* for some reason the -90deg orientation doesn't match the -webkit-linear-gradient. It should be 180deg here.
* webkit also insists on rendering *both* gradients instead of only the native one. So it doesn't looks right. ugh.
background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); */
/*background:#FFF;*/
padding: 3px;
padding-top: 15px;
@ -607,7 +611,9 @@
background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
/* troublesome in latest webkit
background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
*/
/*background:#FFF;*/
padding: 3px;
padding-top:15px;
@ -991,12 +997,20 @@
margin-bottom:10px;
}
.point-of-sale .order .summary .line{
float: right;
margin-right:15px;
margin-left: 15px;
padding-top:5px;
border-top: solid 2px;
border-color:#777;
}
.point-of-sale .order .summary .line .subentry{
font-size: 10px;
font-weight: normal;
text-align: center;
}
.point-of-sale .order .summary .line.empty{
text-align: right;
border-color:#BBB;
color:#999;
}

View File

@ -40,6 +40,10 @@ function openerp_pos_db(instance, module){
//cache the data in memory to avoid roundtrips to the localstorage
this.cache = {};
this.product_by_id = {};
this.product_by_ean13 = {};
this.product_by_category_id = {};
this.category_by_id = {};
this.root_category_id = 0;
this.category_products = {};
@ -49,6 +53,7 @@ function openerp_pos_db(instance, module){
this.category_search_string = {};
this.packagings_by_id = {};
this.packagings_by_product_id = {};
this.packagings_by_ean13 = {};
},
/* returns the category object from its id. If you pass a list of id as parameters, you get
* a list of category objects.
@ -137,7 +142,6 @@ function openerp_pos_db(instance, module){
/* saves a record store to the database */
save: function(store,data){
var str_data = JSON.stringify(data);
console.log('Storing '+ Math.round(str_data.length/1024.0)+' KB of data to store: '+store);
localStorage[this.name + '_' + store] = JSON.stringify(data);
this.cache[store] = data;
},
@ -153,8 +157,7 @@ function openerp_pos_db(instance, module){
return str + '\n';
},
add_products: function(products){
var stored_products = this.load('products',{});
var stored_categories = this.load('categories',{});
var stored_categories = this.product_by_category_id;
if(!products instanceof Array){
products = [products];
@ -187,10 +190,11 @@ function openerp_pos_db(instance, module){
}
this.category_search_string[ancestor] += search_string;
}
stored_products[product.id] = product;
this.product_by_id[product.id] = product;
if(product.ean13){
this.product_by_ean13[product.ean13] = product;
}
}
this.save('products',stored_products);
this.save('categories',stored_categories);
},
add_packagings: function(packagings){
for(var i = 0, len = packagings.length; i < len; i++){
@ -200,6 +204,9 @@ function openerp_pos_db(instance, module){
this.packagings_by_product_id[pack.product_id[0]] = [];
}
this.packagings_by_product_id[pack.product_id[0]].push(pack);
if(pack.ean13){
this.packagings_by_ean13[pack.ean13] = pack;
}
}
},
/* removes all the data from the database. TODO : being able to selectively remove data */
@ -219,31 +226,24 @@ function openerp_pos_db(instance, module){
return count;
},
get_product_by_id: function(id){
return this.load('products',{})[id];
return this.product_by_id[id];
},
get_product_by_ean13: function(ean13){
var products = this.load('products',{});
for(var i in products){
if( products[i] && products[i].ean13 === ean13){
return products[i];
}
if(this.product_by_ean13[ean13]){
return this.product_by_ean13[ean13];
}
for(var p in this.packagings_by_id){
var pack = this.packagings_by_id[p];
if( pack.ean === ean13){
return products[pack.product_id[0]];
}
var pack = this.packagings_by_ean13[ean13];
if(pack){
return this.product_by_id[pack.product_id[0]];
}
return undefined;
},
get_product_by_category: function(category_id){
var stored_categories = this.load('categories',{});
var stored_products = this.load('products',{});
var product_ids = stored_categories[category_id];
var product_ids = this.product_by_category_id[category_id];
var list = [];
if (product_ids) {
for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) {
list.push(stored_products[product_ids[i]]);
list.push(this.product_by_id[product_ids[i]]);
}
}
return list;
@ -275,12 +275,9 @@ function openerp_pos_db(instance, module){
},
remove_order: function(order_id){
var orders = this.load('orders',[]);
console.log('Remove order:',order_id);
console.log('Order count:',orders.length);
orders = _.filter(orders, function(order){
return order.id !== order_id;
});
console.log('Order count:',orders.length);
this.save('orders',orders);
},
get_orders: function(){

View File

@ -1,6 +1,24 @@
function openerp_pos_models(instance, module){ //module is instance.point_of_sale
var QWeb = instance.web.qweb;
// rounds a value with a fixed number of decimals.
// round(3.141492,2) -> 3.14
function round(value,decimals){
var mult = Math.pow(10,decimals || 0);
return Math.round(value*mult)/mult;
}
window.round = round;
// rounds a value with decimal form precision
// round(3.141592,0.025) ->3.125
function round_pr(value,precision){
if(!precision || precision < 0){
throw new Error('round_pr(): needs a precision greater than zero, got '+precision+' instead');
}
return Math.round(value / precision) * precision;
}
window.round_pr = round_pr;
// The PosModel contains the Point Of Sale's representation of the backend.
// Since the PoS must work in standalone ( Without connection to the server )
@ -24,8 +42,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
this.db = new module.PosLS(); // a database used to store the products and categories
this.db.clear('products','categories');
this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
// default attributes values. If null, it will be loaded below.
this.set({
@ -101,8 +118,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).then(function(company_partners){
self.get('company').contact_address = company_partners[0].contact_address;
return self.fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]);
return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('company').currency_id[0]]]);
}).then(function(currencies){
console.log('Currency:',currencies[0]);
self.set('currency',currencies[0]);
return self.fetch('product.uom', null, null);
@ -117,7 +135,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return self.fetch('product.packaging', null, null);
}).then(function(packagings){
self.set('product.packaging',packagings);
return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
}).then(function(users){
self.set('user_list',users);
@ -211,7 +229,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// logs the usefull posmodel data to the console for debug purposes
log_loaded_data: function(){
console.log('PosModel data has been loaded:');
console.log('PosModel: categories:',this.get('categories'));
console.log('PosModel: units:',this.get('units'));
console.log('PosModel: bank_statements:',this.get('bank_statements'));
console.log('PosModel: journals:',this.get('journals'));
@ -339,19 +356,26 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.product = options.product;
this.price = options.product.get('price');
this.quantity = 1;
this.quantityStr = '1';
this.discount = 0;
this.discountStr = '0';
this.type = 'unit';
this.selected = false;
},
// sets a discount [0,100]%
set_discount: function(discount){
this.discount = Math.max(0,Math.min(100,discount));
var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100);
this.discount = disc;
this.discountStr = '' + disc;
this.trigger('change');
},
// returns the discount [0,100]%
get_discount: function(){
return this.discount;
},
get_discount_str: function(){
return this.discountStr;
},
get_product_type: function(){
return this.type;
},
@ -359,13 +383,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// product's unity of measure properties. Quantities greater than zero will not get
// rounded to zero
set_quantity: function(quantity){
if(_.isNaN(quantity)){
if(quantity === 'remove'){
this.order.removeOrderline(this);
}else if(quantity !== undefined){
this.quantity = Math.max(0,quantity);
return;
}else{
var quant = Math.max(parseFloat(quantity) || 0, 0);
var unit = this.get_unit();
if(unit && this.quantity > 0 ){
this.quantity = Math.max(unit.rounding, Math.round(quantity / unit.rounding) * unit.rounding);
if(unit){
this.quantity = Math.max(unit.rounding, Math.round(quant / unit.rounding) * unit.rounding);
this.quantityStr = this.quantity.toFixed(Math.max(0,Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))));
}else{
this.quantity = quant;
this.quantityStr = '' + this.quantity;
}
}
this.trigger('change');
@ -374,6 +403,17 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_quantity: function(){
return this.quantity;
},
get_quantity_str: function(){
return this.quantityStr;
},
get_quantity_str_with_unit: function(){
var unit = this.get_unit();
if(unit && unit.name !== 'Unit(s)'){
return this.quantityStr + ' ' + unit.name;
}else{
return this.quantityStr;
}
},
// return the unit of measure of the product
get_unit: function(){
var unit_id = (this.product.get('uos_id') || this.product.get('uom_id'));
@ -390,15 +430,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_product: function(){
return this.product;
},
// return the base price of this product (for this orderline)
get_price: function(){
return this.price;
},
// changes the base price of the product for this orderline
set_price: function(price){
this.price = price;
this.trigger('change');
},
// selects or deselects this orderline
set_selected: function(selected){
this.selected = selected;
@ -429,7 +460,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
export_as_JSON: function() {
return {
qty: this.get_quantity(),
price_unit: this.get_price(),
price_unit: this.get_unit_price(),
discount: this.get_discount(),
product_id: this.get_product().get('id'),
};
@ -439,9 +470,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return {
quantity: this.get_quantity(),
unit_name: this.get_unit().name,
price: this.get_price(),
price: this.get_unit_price(),
discount: this.get_discount(),
product_name: this.get_product().get('name'),
price_display : this.get_display_price(),
price_with_tax : this.get_price_with_tax(),
price_without_tax: this.get_price_without_tax(),
tax: this.get_tax(),
@ -449,6 +481,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
product_description_sale: this.get_product().get('description_sale'),
};
},
// changes the base price of the product for this orderline
set_unit_price: function(price){
this.price = round(parseFloat(price) || 0, 2);
this.trigger('change');
},
get_unit_price: function(){
var rounding = this.pos.get('currency').rounding;
return round_pr(this.price,rounding);
},
get_display_price: function(){
var rounding = this.pos.get('currency').rounding;
return round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding);
},
get_price_without_tax: function(){
return this.get_all_prices().priceWithoutTax;
},
@ -458,9 +503,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_tax: function(){
return this.get_all_prices().tax;
},
get_all_prices: function() {
get_all_prices: function(){
var self = this;
var base = this.get_quantity() * this.price * (1 - (this.get_discount() / 100));
var currency_rounding = this.pos.get('currency').rounding;
var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
var totalTax = base;
var totalNoTax = base;
@ -474,12 +520,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
if (tax.price_include) {
var tmp;
if (tax.type === "percent") {
tmp = base - (base / (1 + tax.amount));
tmp = base - round_pr(base / (1 + tax.amount),currency_rounding);
} else if (tax.type === "fixed") {
tmp = tax.amount * self.get_quantity();
tmp = round_pr(tax.amount * self.get_quantity(),currency_rounding);
} else {
throw "This type of tax is not supported by the point of sale: " + tax.type;
}
tmp = round_pr(tmp,currency_rounding);
taxtotal += tmp;
totalNoTax -= tmp;
} else {
@ -491,6 +538,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
} else {
throw "This type of tax is not supported by the point of sale: " + tax.type;
}
tmp = round_pr(tmp,currency_rounding);
taxtotal += tmp;
totalTax += tmp;
}
@ -515,7 +563,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
},
//sets the amount of money on this payment line
set_amount: function(value){
this.amount = value;
this.amount = parseFloat(value) || 0;
this.trigger('change');
},
// returns the amount of money on this paymentline
@ -584,7 +632,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
line.set_quantity(options.quantity);
}
if(options.price !== undefined){
line.set_price(options.price);
line.set_unit_price(options.price);
}
var last_orderline = this.getLastOrderline();
@ -613,14 +661,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
getName: function() {
return this.get('name');
},
getTotal: function() {
getSubtotal : function(){
return (this.get('orderLines')).reduce((function(sum, orderLine){
return sum + orderLine.get_display_price();
}), 0);
},
getTotalTaxIncluded: function() {
return (this.get('orderLines')).reduce((function(sum, orderLine) {
return sum + orderLine.get_price_with_tax();
}), 0);
},
getDiscountTotal: function() {
return (this.get('orderLines')).reduce((function(sum, orderLine) {
return sum + (orderLine.get_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
}), 0);
},
getTotalTaxExcluded: function() {
@ -639,10 +692,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}), 0);
},
getChange: function() {
return this.getPaidTotal() - this.getTotal();
return this.getPaidTotal() - this.getTotalTaxIncluded();
},
getDueLeft: function() {
return this.getTotal() - this.getPaidTotal();
return this.getTotalTaxIncluded() - this.getPaidTotal();
},
// sets the type of receipt 'receipt'(default) or 'invoice'
set_receipt_type: function(type){
@ -698,10 +751,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return {
orderlines: orderlines,
paymentlines: paymentlines,
total_with_tax: this.getTotal(),
subtotal: this.getSubtotal(),
total_with_tax: this.getTotalTaxIncluded(),
total_without_tax: this.getTotalTaxExcluded(),
total_tax: this.getTax(),
total_paid: this.getPaidTotal(),
total_discount: this.getDiscountTotal(),
change: this.getChange(),
name : this.getName(),
client: client ? client.name : null ,
@ -743,7 +798,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
return {
name: this.getName(),
amount_paid: this.getPaidTotal(),
amount_total: this.getTotal(),
amount_total: this.getTotalTaxIncluded(),
amount_tax: this.getTax(),
amount_return: this.getChange(),
lines: orderLines,
@ -800,20 +855,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
buffer: (this.get('buffer')) + newChar
});
}
this.updateTarget();
this.trigger('set_value',this.get('buffer'));
},
deleteLastChar: function() {
var tempNewBuffer = this.get('buffer').slice(0, -1);
if(!tempNewBuffer){
this.set({ buffer: "0" });
this.killTarget();
}else{
if (isNaN(tempNewBuffer)) {
tempNewBuffer = "0";
if(this.get('buffer') === ""){
if(this.get('mode') === 'quantity'){
this.trigger('set_value','remove');
}else{
this.trigger('set_value',this.get('buffer'));
}
this.set({ buffer: tempNewBuffer });
this.updateTarget();
}else{
var newBuffer = this.get('buffer').slice(0,-1) || "";
this.set({ buffer: newBuffer });
this.trigger('set_value',this.get('buffer'));
}
},
switchSign: function() {
@ -822,7 +876,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.set({
buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
});
this.updateTarget();
this.trigger('set_value',this.get('buffer'));
},
changeMode: function(newMode) {
this.set({
@ -836,15 +890,8 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
mode: "quantity"
});
},
updateTarget: function() {
var bufferContent, params;
bufferContent = this.get('buffer');
if (bufferContent && !isNaN(bufferContent)) {
this.trigger('set_value', parseFloat(bufferContent));
}
},
killTarget: function(){
this.trigger('set_value',Number.NaN);
resetValue: function(){
this.set({buffer:'0'});
},
});
}

View File

@ -368,7 +368,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
template:'ChooseReceiptPopupWidget',
show: function(){
console.log('show');
this._super();
this.renderElement();
var self = this;
@ -603,7 +602,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
// initiates the connection to the payment terminal and starts the update requests
this.start = function(){
var def = new $.Deferred();
console.log("START");
self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft())
.done(function(ack){
if(ack === 'ok'){
@ -613,7 +611,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}else{
console.error('unknown payment request return value:',ack);
}
console.log("START_END");
def.resolve();
});
return def;
@ -621,10 +618,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
// gets updated status from the payment terminal and performs the appropriate consequences
this.update = function(){
console.log("UPDATE");
var def = new $.Deferred();
if(self.canceled){
console.log("UPDATE_END");
return def.resolve();
}
self.pos.proxy.payment_status()
@ -656,7 +651,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}else{
console.error('unknown status value:',status.status);
}
console.log("UPDATE_END");
def.resolve();
});
return def;
@ -664,14 +658,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
// cancels a payment.
this.cancel = function(){
console.log("CANCEL");
if(!self.paid && !self.canceled){
self.canceled = true;
self.pos.proxy.payment_cancel();
self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
self.queue.clear();
}
console.log("CANCEL_END");
return (new $.Deferred()).resolve();
}
@ -865,6 +857,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.bindPaymentLineEvents();
this.bind_orderline_events();
this.paymentlinewidgets = [];
this.focusedLine = null;
},
show: function(){
this._super();
@ -894,6 +887,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
this.updatePaymentSummary();
this.line_refocus();
},
close: function(){
this._super();
@ -931,17 +925,30 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.bind_orderline_events();
this.renderElement();
},
line_refocus: function(lineWidget){
if(lineWidget){
if(this.focusedLine !== lineWidget){
this.focusedLine = lineWidget;
}
}
if(this.focusedLine){
this.focusedLine.focus();
}
},
addPaymentLine: function(newPaymentLine) {
var self = this;
var l = new module.PaymentlineWidget(null, {
payment_line: newPaymentLine
var l = new module.PaymentlineWidget(this, {
payment_line: newPaymentLine,
});
l.on('delete_payment_line', self, function(r) {
self.deleteLine(r);
});
l.appendTo(this.$('#paymentlines'));
this.paymentlinewidgets.push(l);
this.$('.paymentline-amount input:last').focus();
if(this.numpadState){
this.numpadState.resetValue();
}
this.line_refocus(l);
},
renderElement: function() {
this._super();
@ -958,25 +965,26 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
deleteLine: function(lineWidget) {
this.currentPaymentLines.remove([lineWidget.payment_line]);
lineWidget.destroy();
},
updatePaymentSummary: function() {
var currentOrder = this.pos.get('selectedOrder');
var paidTotal = currentOrder.getPaidTotal();
var dueTotal = currentOrder.getTotal();
var dueTotal = currentOrder.getTotalTaxIncluded();
var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
this.$('#payment-due-total').html(dueTotal.toFixed(2));
this.$('#payment-paid-total').html(paidTotal.toFixed(2));
this.$('#payment-remaining').html(remaining.toFixed(2));
this.$('#payment-change').html(change.toFixed(2));
if((currentOrder.selected_orderline == undefined))
remaining = 1
this.$('#payment-due-total').html(this.format_currency(dueTotal));
this.$('#payment-paid-total').html(this.format_currency(paidTotal));
this.$('#payment-remaining').html(this.format_currency(remaining));
this.$('#payment-change').html(this.format_currency(change));
if(currentOrder.selected_orderline === undefined){
remaining = 1; // What is this ?
}
if(this.pos_widget.action_bar){
this.pos_widget.action_bar.set_button_disabled('validation', remaining > 0);
}
this.$('.paymentline-amount input:last').focus();
},
set_numpad_state: function(numpadState) {
if (this.numpadState) {
@ -998,5 +1006,4 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.currentPaymentLines.last().set_amount(val);
},
});
}

View File

@ -22,14 +22,20 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of
if(this.pos && this.pos.get('currency')){
this.currency = this.pos.get('currency');
}else{
this.currency = {symbol: '$', position: 'after'};
this.currency = {symbol: '$', position: 'after', rounding: 0.01};
}
var decimals = Math.max(0,Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10)));
this.format_currency = function(amount){
if(typeof amount === 'number'){
amount = Math.round(amount*100)/100;
amount = amount.toFixed(decimals);
}
if(this.currency.position === 'after'){
return Math.round(amount*100)/100 + ' ' + this.currency.symbol;
return amount + ' ' + this.currency.symbol;
}else{
return this.currency.symbol + ' ' + Math.round(amount*100)/100;
return this.currency.symbol + ' ' + amount;
}
}

View File

@ -186,7 +186,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
}else if( mode === 'discount'){
order.getSelectedLine().set_discount(val);
}else if( mode === 'price'){
order.getSelectedLine().set_price(val);
order.getSelectedLine().set_unit_price(val);
}
} else {
this.pos.get('selectedOrder').destroy();
@ -269,8 +269,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
update_summary: function(){
var order = this.pos.get('selectedOrder');
var total = order ? order.getTotal() : 0;
this.$('.summary .value.total').html(this.format_currency(total));
var total = order ? order.getTotalTaxIncluded() : 0;
var taxes = order ? total - order.getTotalTaxExcluded() : 0;
this.$('.summary .total > .value').html(this.format_currency(total));
this.$('.summary .total .subentry .value').html(this.format_currency(taxes));
},
set_display_mode: function(mode){
if(this.display_mode !== mode){
@ -311,24 +313,34 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
changeAmount: function(event) {
var newAmount = event.currentTarget.value;
if (newAmount && !isNaN(newAmount)) {
this.amount = parseFloat(newAmount);
this.payment_line.set_amount(this.amount);
var amount = parseFloat(newAmount);
if(!isNaN(amount)){
this.amount = amount;
this.payment_line.set_amount(amount);
}
},
changedAmount: function() {
if (this.amount !== this.payment_line.get_amount())
if (this.amount !== this.payment_line.get_amount()){
this.renderElement();
}
},
renderElement: function() {
var self = this;
this.name = this.payment_line.get_cashregister().get('journal_id')[1];
this._super();
this.$('input').keyup(_.bind(this.changeAmount, this));
this.$('input').keyup(function(event){
self.changeAmount(event);
});
this.$('.delete-payment-line').click(function() {
self.trigger('delete_payment_line', self);
});
},
focus: function(){
var val = this.$('input')[0].value;
this.$('input')[0].focus();
this.$('input')[0].value = val;
this.$('input')[0].select();
},
});
module.OrderButtonWidget = module.PosBaseWidget.extend({
@ -606,20 +618,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
if(this.scrollbar){
this.scrollbar.destroy();
}
this.pos.get('products')
.chain()
.map(function(product) {
var product = new module.ProductWidget(self, {
model: product,
weight: self.weight,
click_product_action: self.click_product_action,
})
self.productwidgets.push(product);
return product;
})
.invoke('appendTo', this.$('.product-list'));
var products = this.pos.get('products').models || [];
for(var i = 0, len = products.length; i < len; i++){
var product = new module.ProductWidget(self, {
model: products[i],
click_product_action: this.click_product_action,
});
this.productwidgets.push(product);
product.appendTo(this.$('.product-list'));
}
this.scrollbar = new module.ScrollbarWidget(this,{
target_widget: this,
target_selector: '.product-list-scroller',

View File

@ -48,11 +48,17 @@
</t>
<t t-name="SynchNotificationWidget">
<div class="oe_pos_synch-notification">
<t t-if="widget.get_nbr_pending() > 0" t-esc="widget.get_nbr_pending()"/>
<div t-if="widget.get_nbr_pending() > 0" class="oe_status_red"></div>
<div t-if="widget.get_nbr_pending() === 0" class="oe_status_green"></div>
</div>
<t t-if="widget.get_nbr_pending() > 0">
<div class="oe_pos_synch-notification">
<t t-esc="widget.get_nbr_pending()"/>
<div class="oe_status_red"></div>
</div>
</t>
<t t-if="widget.get_nbr_pending() === 0">
<div class="oe_pos_synch-notification oe_inactive">
<div class="oe_status_green"></div>
</div>
</t>
</t>
<t t-name="HeaderButtonWidget">
@ -214,11 +220,7 @@
<span class="left-block">
Total:
</span>
<span class="right-block">
<t t-if="widget.currency.position == 'before'" t-esc="widget.currency.symbol"/>
<span id="payment-due-total"></span>
<t t-if="widget.currency.position == 'after'" t-esc="widget.currency.symbol"/>
</span>
<span class='right-block' id="payment-due-total"></span>
</div>
<table id="paymentlines">
</table>
@ -227,31 +229,19 @@
<span class='left-block'>
Paid:
</span>
<span class='right-block'>
<t t-if="widget.currency.position == 'before'" t-esc="widget.currency.symbol"/>
<span id="payment-paid-total"></span>
<t t-if="widget.currency.position == 'after'" t-esc="widget.currency.symbol"/>
</span>
<span class='right-block' id="payment-paid-total"></span>
</div>
<div class="infoline">
<span class='left-block'>
Remaining:
</span>
<span class='right-block'>
<t t-if="widget.currency.position == 'before'" t-esc="widget.currency.symbol"/>
<span id="payment-remaining"></span>
<t t-if="widget.currency.position == 'after'" t-esc="widget.currency.symbol"/>
</span>
<span class='right-block' id="payment-remaining"></span>
</div>
<div class="infoline" >
<span class='left-block'>
Change:
</span>
<span class='right-block'>
<t t-if="widget.currency.position == 'before'" t-esc="widget.currency.symbol"/>
<span id="payment-change"></span>
<t t-if="widget.currency.position == 'after'" t-esc="widget.currency.symbol"/>
</span>
<span class='right-block' id="payment-change"></span>
</div>
</div>
</div>
@ -431,9 +421,13 @@
</ul>
<div class="summary">
<span t-attf-class="line #{widget.pos.get('selectedOrder').get('orderLines').length === 0 ? 'empty' : ''}">
<span class="label total">Total:</span> <span class="value total">0.00 €</span>
</span>
<div t-attf-class="line #{widget.pos.get('selectedOrder').get('orderLines').length === 0 ? 'empty' : ''}">
<div class='entry total'>
<span class="label">Total: </span> <span class="value">0.00 €</span>
<div class='subentry'>Taxes: <span class="value">0.00€</span></div>
</div>
</div>
<div class='clear'></div>
</div>
</div>
</div>
@ -500,26 +494,26 @@
<t t-esc="widget.model.get_product().get('name')"/>
</span>
<span class="price">
<t t-esc="widget.format_currency(widget.model.get_price_with_tax())"/>
<t t-esc="widget.format_currency(widget.model.get_display_price())"/>
</span>
<ul class="info-list">
<t t-if="widget.model.get_quantity() !== 1.0">
<t t-if="widget.model.get_quantity_str() !== '1'">
<li class="info">
<em>
<t t-esc="widget.model.get_quantity()" />
<t t-esc="widget.model.get_quantity_str()" />
</em>
<t t-esc="widget.model.get_unit().name" />
at
<t t-esc="widget.format_currency(widget.model.get_price())" />
<t t-esc="widget.format_currency(widget.model.get_unit_price())" />
/
<t t-esc="widget.model.get_unit().name" />
</li>
</t>
<t t-if="widget.model.get_discount() > 0">
<t t-if="widget.model.get_discount_str() !== '0'">
<li class="info">
With a
<em>
<t t-esc="widget.model.get_discount()" />%
<t t-esc="widget.model.get_discount_str()" />%
</em>
discount
</li>
@ -571,33 +565,36 @@
Shop: <t t-esc="widget.shop_obj.name"/><br />
<br />
<table>
<tr t-foreach="widget.currentOrderLines.toArray()" t-as="order">
<tr t-foreach="widget.currentOrderLines.toArray()" t-as="orderline">
<td>
<t t-esc="order.get_product().get('name')"/>
<t t-if="order.get_discount() > 0">
<t t-esc="orderline.get_product().get('name')"/>
<t t-if="orderline.get_discount() > 0">
<div class="pos-disc-font">
With a <t t-esc="order.get_discount()"/>% discount
With a <t t-esc="orderline.get_discount()"/>% discount
</div>
</t>
</td>
<td class="pos-right-align">
<t t-esc="order.get_quantity().toFixed(0)"/>
<t t-esc="orderline.get_quantity_str_with_unit()"/>
</td>
<td class="pos-right-align">
<t t-esc="widget.format_currency(order.get_price() * (1 - order.get_discount()/100) * order.get_quantity().toFixed(2))"/>
<t t-esc="widget.format_currency(orderline.get_display_price())"/>
</td>
</tr>
</table>
<br />
<table>
<tr><td>Subtotal:</td><td class="pos-right-align">
<t t-esc="widget.format_currency(widget.currentOrder.getSubtotal())"/>
</td></tr>
<tr><td>Tax:</td><td class="pos-right-align">
<t t-esc="widget.format_currency(widget.currentOrder.getTax().toFixed(2))"/>
<t t-esc="widget.format_currency(widget.currentOrder.getTax())"/>
</td></tr>
<tr><td>Discount:</td><td class="pos-right-align">
<t t-esc="widget.format_currency(widget.currentOrder.getDiscountTotal().toFixed(2))"/>
<t t-esc="widget.format_currency(widget.currentOrder.getDiscountTotal())"/>
</td></tr>
<tr class="emph"><td>Total:</td><td class="pos-right-align">
<t t-esc="widget.format_currency(widget.currentOrder.getTotal().toFixed(2))"/>
<t t-esc="widget.format_currency(widget.currentOrder.getTotalTaxIncluded())"/>
</td></tr>
</table>
<br />
@ -607,14 +604,14 @@
<t t-esc="pline.get_cashregister().get('journal_id')[1]"/>
</td>
<td class="pos-right-align">
<t t-esc="widget.format_currency((pline.get_amount()).toFixed(2))"/>
<t t-esc="widget.format_currency(pline.get_amount())"/>
</td>
</tr>
</table>
<br />
<table>
<tr><td>Change:</td><td class="pos-right-align">
<t t-esc="widget.format_currency((widget.currentOrder.getPaidTotal() - widget.currentOrder.getTotal()).toFixed(2))"/>
<t t-esc="widget.format_currency(widget.currentOrder.getPaidTotal() - widget.currentOrder.getTotalTaxIncluded())"/>
</td></tr>
</table>
</div>

View File

@ -103,7 +103,7 @@ class procurement_order(osv.osv):
readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \
" a make to order method."),
'note': fields.text('Note'),
'message': fields.char('Latest error', size=124, help="Exception occurred while computing procurement orders."),
'message': fields.char('Latest error', help="Exception occurred while computing procurement orders."),
'state': fields.selection([
('draft','Draft'),
('cancel','Cancelled'),
@ -367,10 +367,22 @@ class procurement_order(osv.osv):
if message:
message = _("Procurement '%s' is in exception: ") % (procurement.name) + message
cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
#temporary context passed in write to prevent an infinite loop
ctx_wkf = dict(context or {})
ctx_wkf['workflow.trg_write.%s' % self._name] = False
self.write(cr, uid, [procurement.id], {'message': message},context=ctx_wkf)
self.message_post(cr, uid, [procurement.id], body=message, context=context)
return ok
def _workflow_trigger(self, cr, uid, ids, trigger, context=None):
""" Don't trigger workflow for the element specified in trigger
"""
wkf_op_key = 'workflow.%s.%s' % (trigger, self._name)
if context and not context.get(wkf_op_key, True):
# make sure we don't have a trigger loop while processing triggers
return
return super(procurement_order,self)._workflow_trigger(cr, uid, ids, trigger, context=context)
def action_produce_assign_service(self, cr, uid, ids, context=None):
""" Changes procurement state to Running.
@return: True

View File

@ -33,7 +33,10 @@
'images/project_task_tree.jpeg',
'images/project_task.jpeg',
'images/project.jpeg',
'images/task_analysis.jpeg'
'images/task_analysis.jpeg',
'images/project_kanban.jpeg',
'images/task_kanban.jpeg',
'images/task_stages.jpeg'
],
'depends': [
'base_setup',

View File

@ -94,16 +94,19 @@
<record id="mt_task_blocked" model="mail.message.subtype">
<field name="name">Task Blocked</field>
<field name="res_model">project.task</field>
<field name="default" eval="False"/>
<field name="description">Task blocked</field>
</record>
<record id="mt_task_closed" model="mail.message.subtype">
<field name="name">Task Done</field>
<field name="res_model">project.task</field>
<field name="default" eval="False"/>
<field name="description">Task closed</field>
</record>
<record id="mt_task_stage" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<field name="res_model">project.task</field>
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<!-- Project-related subtypes for messaging / Chatter -->

View File

@ -501,6 +501,7 @@ class project_issue(base_stage, osv.osv):
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'partner_id': msg.get('author_id', False),
'user_id': False,
}
if msg.get('priority'):

View File

@ -59,16 +59,19 @@ Access all issues from the top Project menu, and access the issues of a specific
<record id="mt_issue_blocked" model="mail.message.subtype">
<field name="name">Issue Blocked</field>
<field name="res_model">project.issue</field>
<field name="default" eval="False"/>
<field name="description">Issue blocked</field>
</record>
<record id="mt_issue_closed" model="mail.message.subtype">
<field name="name">Issue Closed</field>
<field name="res_model">project.issue</field>
<field name="default" eval="False"/>
<field name="description">Issue closed</field>
</record>
<record id="mt_issue_stage" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<field name="res_model">project.issue</field>
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<!-- Project-related subtypes for messaging / Chatter -->

View File

@ -52,10 +52,12 @@
<!-- Purchase-related subtypes for messaging / Chatter -->
<record id="mt_rfq_confirmed" model="mail.message.subtype">
<field name="name">RFQ Confirmed</field>
<field name="default" eval="False"/>
<field name="res_model">purchase.order</field>
</record>
<record id="mt_rfq_approved" model="mail.message.subtype">
<field name="name">RFQ Approved</field>
<field name="default" eval="False"/>
<field name="res_model">purchase.order</field>
</record>

View File

@ -19,10 +19,14 @@
#
##############################################################################
import logging
from openerp.osv import fields, osv
from openerp import pooler
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class sale_configuration(osv.osv_memory):
_inherit = 'sale.config.settings'
@ -81,8 +85,12 @@ Example: Product: this product is deprecated, do not purchase more than 5.
user = self.pool.get('res.users').browse(cr, uid, uid, context)
res['time_unit'] = user.company_id.project_time_mode_id.id
else:
product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant')
res['time_unit'] = product.uom_id.id
try:
product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant')
res['time_unit'] = product.uom_id.id
except ValueError:
# keep default value in that case
_logger.warning("Product with xml_id 'product.product_product_consultant' not found")
return res
def _get_default_time_unit(self, cr, uid, context=None):
@ -98,16 +106,11 @@ Example: Product: this product is deprecated, do not purchase more than 5.
wizard = self.browse(cr, uid, ids)[0]
if wizard.time_unit:
product = False
try:
product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant')
except:
#product with xml_id product_product_consultant has not been found. Don't do anything except logging the exception
import logging
_logger = logging.getLogger(__name__)
_logger.warning("Warning, product with xml_id 'product_product_consultant' hasn't been found")
if product:
product.write({'uom_id': wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
except ValueError:
_logger.warning("Product with xml_id 'product.product_product_consultant' not found, UoMs not updated!")
if wizard.module_project and wizard.time_unit:
user = self.pool.get('res.users').browse(cr, uid, uid, context)

View File

@ -926,7 +926,6 @@ class sale_order_line(osv.osv):
elif uom: # whether uos is set or not
default_uom = product_obj.uom_id and product_obj.uom_id.id
q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom)
result['product_uom'] = default_uom
if product_obj.uos_id:
result['product_uos'] = product_obj.uos_id.id
result['product_uos_qty'] = qty * product_obj.uos_coeff

View File

@ -48,11 +48,13 @@
<record id="mt_order_sent" model="mail.message.subtype">
<field name="name">Quotation send</field>
<field name="res_model">sale.order</field>
<field name="default" eval="False"/>
<field name="description">Quotation send</field>
</record>
<record id="mt_order_confirmed" model="mail.message.subtype">
<field name="name">Sales Order Confirmed</field>
<field name="res_model">sale.order</field>
<field name="default" eval="False"/>
<field name="description">Quotation confirmed</field>
</record>

View File

@ -14,3 +14,23 @@
!assert {model: sale.order, id: sale.sale_order_test1, string: The onchange function of product was not correctly triggered}:
- order_line[0].name == u'[LCD17] 17\u201d LCD Monitor'
- order_line[0].price_unit == 1350.0
- order_line[0].product_uom_qty == 8
- order_line[0].product_uom.id == ref('product.product_uom_unit')
-
I create another sale order
-
!record {model: sale.order, id: sale_order_test2}:
partner_id: base.res_partner_2
order_line:
- product_id: product.product_product_7
product_uom_qty: 16
product_uom: product.product_uom_dozen
-
I verify that the onchange was correctly triggered
-
!assert {model: sale.order, id: sale.sale_order_test2, string: The onchange function of product was not correctly triggered}:
- order_line[0].name == u'[LCD17] 17\u201d LCD Monitor'
- order_line[0].price_unit == 1350.0 * 12
- order_line[0].product_uom.id == ref('product.product_uom_dozen')
- order_line[0].product_uom_qty == 16

View File

@ -91,7 +91,7 @@
<field name="group_uos" class="oe_inline"/>
<label for="group_uos" />
</div>
<div>
<div invisible="1">
<field name="group_product_variant" class="oe_inline"/>
<label for="group_product_variant"/>
</div>

View File

@ -16,7 +16,7 @@ openerp.web_analytics = function(instance) {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
ga.src = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga,s);
})();
@ -25,16 +25,15 @@ openerp.web_analytics = function(instance) {
/*
* This method initializes the tracker
*/
init: function() {
/* Comment this lines when going on production, only used for testing on localhost
_gaq.push(['_setAccount', 'UA-35793871-1']);
_gaq.push(['_setDomainName', 'none']);
*/
/* Uncomment this lines when going on production */
init: function(webclient) {
var self = this;
self.initialized = $.Deferred();
_gaq.push(['_setAccount', 'UA-7333765-1']);
_gaq.push(['_setDomainName', '.openerp.com']); // Allow multi-domain
/**/
self.initialize_custom(webclient).then(function() {
webclient.on('state_pushed', self, self.on_state_pushed);
self.include_tracker();
});
},
/*
* This method MUST be overriden by saas_demo and saas_trial in order to
@ -48,41 +47,53 @@ openerp.web_analytics = function(instance) {
* This method gets the user access level, to be used as CV in GA
*/
_get_user_access_level: function() {
if (!instance.session.session_is_valid()) {
return "Unauthenticated User";
}
if (instance.session.uid === 1) {
return 'Admin User';
// Make the difference between portal users and anonymous users
} else if (instance.session.username.indexOf('@') !== -1) {
if (instance.session.username.indexOf('anonymous') === -1) {
return 'Portal User';
} else {
return 'Anonymous User';
}
} else if (instance.session.username.indexOf('anonymous') !== -1) {
return 'Anonymous User';
} else {
return 'Normal User';
}
// Make the difference between portal users and anonymous users
if (instance.session.username.indexOf('@') !== -1) {
return 'Portal User';
}
if (instance.session.username === 'anonymous') {
return 'Anonymous User';
}
return 'Normal User';
},
/*
* This method contains the initialization of all user-related custom variables
* stored in GA. Also other modules can override it to add new custom variables
* Must be followed by a call to _push_*() in order to actually send the data
* to GA.
*/
initialize_custom: function() {
var self = this;
return instance.session.rpc("/web/webclient/version_info", {})
instance.session.rpc("/web/webclient/version_info", {})
.done(function(res) {
_gaq.push(['_setCustomVar', 5, 'Version', res.server_version, 3]);
// Track User Access Level, Custom Variable 4 in GA with visitor level scope
// Values: 'Admin User', 'Normal User', 'Portal User', 'Anonymous User'
_gaq.push(['_setCustomVar', 4, 'User Access Level', self.user_access_level, 1]);
// Track User Type Conversion, Custom Variable 3 in GA with session level scope
// Values: 'Visitor', 'Demo', 'Online Trial', 'Online Paying', 'Local User'
_gaq.push(['_setCustomVar', 1, 'User Type Conversion', self._get_user_type(), 2]);
_gaq.push(['_trackPageview']);
return;
self._push_customvars();
self.initialized.resolve(self);
});
return self.initialized;
},
/*
* Method called in order to send _setCustomVar to GA
*/
_push_customvars: function() {
var self = this;
// Track User Access Level, Custom Variable 4 in GA with visitor level scope
// Values: 'Admin User', 'Normal User', 'Portal User', 'Anonymous User'
_gaq.push(['_setCustomVar', 4, 'User Access Level', self._get_user_access_level(), 1]);
// Track User Type Conversion, Custom Variable 3 in GA with session level scope
// Values: 'Visitor', 'Demo', 'Online Trial', 'Online Paying', 'Local User'
_gaq.push(['_setCustomVar', 1, 'User Type Conversion', self._get_user_type(), 2]);
},
/*
* Method called in order to send _trackPageview to GA
*/
@ -142,6 +153,7 @@ openerp.web_analytics = function(instance) {
'action': state.view_type,
'label': url,
});
this._push_pageview(url);
}
},
/*
@ -157,19 +169,19 @@ openerp.web_analytics = function(instance) {
this._super.apply(this, arguments);
var self = this;
this.on('record_created', self, function(r) {
var url = instance.web_analytics.generateUrl({'model': r.model, 'view_type': 'form'});
var url = instance.web_analytics.generateUrl({'model': self.model, 'view_type': 'form'});
t._push_event({
'category': r.model,
'action': 'form',
'category': self.model,
'action': 'create',
'label': url,
'noninteraction': true,
});
});
this.on('record_saved', self, function(r) {
var url = instance.web_analytics.generateUrl({'model': r.model, 'view_type': 'form'});
var url = instance.web_analytics.generateUrl({'model': self.model, 'view_type': 'form'});
t._push_event({
'category': r.model,
'action': 'form',
'category': self.model,
'action': 'save',
'label': url,
'noninteraction': true,
});
@ -181,12 +193,12 @@ openerp.web_analytics = function(instance) {
instance.web.ActionManager.include({
ir_actions_client: function (action, options) {
var url = instance.web_analytics.generateUrl({'action': action.tag});
var category = action.res_model || action.type;
t._push_event({
'category': action.res_model || action.type,
'action': action.name || action.tag,
'category': action.type,
'action': action.tag,
'label': url,
});
t._push_pageview(url);
return this._super.apply(this, arguments);
},
});
@ -224,21 +236,12 @@ openerp.web_analytics = function(instance) {
options = {'action': params.action};
}
var url = instance.web_analytics.generateUrl(options);
if (error.code) {
t._push_event({
'category': error.message,
'action': error.data.message,
'label': url,
'noninteraction': true,
});
} else {
t._push_event({
'category': error.type,
'action': error.data.debug,
'label': url,
'noninteraction': true,
});
}
t._push_event({
'category': options.model || "ir.actions.client",
'action': "error " + (error.code ? error.message + error.data.message : error.type + error.data.debug),
'label': url,
'noninteraction': true,
});
this._super.apply(this, arguments);
},
});
@ -251,43 +254,37 @@ openerp.web_analytics = function(instance) {
instance.web_analytics.generateUrl = function(options) {
var url = '';
_.each(options, function(value, key) {
url += '/' + key + '=' + value;
var keys = _.keys(options);
keys = _.sortBy(keys, function(i) { return i;});
_.each(keys, function(key) {
url += '/' + key + '/' + options[key];
});
return url;
};
// kept for API compatibility
instance.web_analytics.setupTracker = function(wc) {
var t = wc.tracker;
return $.when(t._get_user_access_level()).then(function(r) {
t.user_access_level = r;
t.initialize_custom().then(function() {
wc.on('state_pushed', t, t.on_state_pushed);
t.include_tracker();
});
});
return wc.tracker.initialized;
};
// Set correctly the tracker in the current instance
if (instance.client instanceof instance.web.WebClient) { // not for embedded clients
instance.webclient.tracker = new instance.web_analytics.Tracker();
instance.web_analytics.setupTracker(instance.webclient);
} else if (!instance.client) {
// client does not already exists, we are in monodb mode
instance.web.WebClient.include({
start: function() {
var d = this._super.apply(this, arguments);
this.tracker = new instance.web_analytics.Tracker();
return d;
},
show_application: function() {
var self = this;
$.when(this.subscribe_deferred).then(function() {
instance.web_analytics.setupTracker(self);
});
this._super();
},
});
}
instance.web.Client.include({
bind_events: function() {
this._super.apply(this, arguments);
this.tracker = new instance.web_analytics.Tracker(this);
},
});
instance.web.Session.include({
session_authenticate: function() {
return $.when(this._super.apply(this, arguments)).then(function() {
// the call to bind_events() may have been delayed in some embed
// cases, and when that happens we can skip the push, as the
// proper one will be done when bind_event is eventually called.
if (instance.client && instance.client.tracker) {
instance.client.tracker._push_customvars();
}
});
},
});
};