[MERGE] forwardport of bugfixes made in v7 up to revision 8854.

bzr revid: qdp-launchpad@openerp.com-20130314112740-s01t51p4m7mxcumz
This commit is contained in:
Quentin (OpenERP) 2013-03-14 12:27:40 +01:00
commit be05b374a1
134 changed files with 1712 additions and 979 deletions

View File

@ -1150,6 +1150,29 @@ class account_move(osv.osv):
_description = "Account Entry" _description = "Account Entry"
_order = 'id desc' _order = 'id desc'
def account_move_prepare(self, cr, uid, journal_id, date=False, ref='', company_id=False, context=None):
'''
Prepares and returns a dictionary of values, ready to be passed to create() based on the parameters received.
'''
if not date:
date = fields.date.today()
period_obj = self.pool.get('account.period')
if not company_id:
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
company_id = user.company_id.id
if context is None:
context = {}
#put the company in context to find the good period
ctx = context.copy()
ctx.update({'company_id': company_id})
return {
'journal_id': journal_id,
'date': date,
'period_id': period_obj.find(cr, uid, date, context=ctx)[0],
'ref': ref,
'company_id': company_id,
}
def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
""" """
Returns a list of tupples containing id, name, as internally it is called {def name_get} Returns a list of tupples containing id, name, as internally it is called {def name_get}
@ -1850,6 +1873,13 @@ class account_tax(osv.osv):
return result in the context return result in the context
Ex: result=round(price_unit*0.21,4) Ex: result=round(price_unit*0.21,4)
""" """
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
name = self.read(cr, uid, id, ['name'], context=context)['name']
default = default.copy()
default.update({'name': name + _(' (Copy)')})
return super(account_tax, self).copy_data(cr, uid, id, default=default, context=context)
def get_precision_tax(): def get_precision_tax():
def change_digit_tax(cr): def change_digit_tax(cr):

View File

@ -22,6 +22,7 @@
import time import time
from lxml import etree from lxml import etree
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
import openerp.exceptions
from openerp import pooler from openerp import pooler
from openerp.osv import fields, osv, orm from openerp.osv import fields, osv, orm
@ -302,16 +303,7 @@ class account_invoice(osv.osv):
('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'), ('number_uniq', 'unique(number, company_id, journal_id, type)', 'Invoice Number must be unique per Company!'),
] ]
def _find_partner(self, inv):
'''
Find the partner for which the accounting entries will be created
'''
#if the chosen partner is not a company and has a parent company, use the parent for the journal entries
#because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
part = inv.partner_id
if part.parent_id and not part.is_company:
part = part.parent_id
return part
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False): def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
@ -981,7 +973,7 @@ class account_invoice(osv.osv):
date = inv.date_invoice or time.strftime('%Y-%m-%d') date = inv.date_invoice or time.strftime('%Y-%m-%d')
part = self._find_partner(inv) part = self.pool.get("res.partner")._find_accounting_partner(inv.partner_id)
line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml) line = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, part.id, date, context=ctx)),iml)
@ -1753,6 +1745,16 @@ class res_partner(osv.osv):
'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True), 'invoice_ids': fields.one2many('account.invoice.line', 'partner_id', 'Invoices', readonly=True),
} }
def _find_accounting_partner(self, part):
'''
Find the partner for which the accounting entries will be created
'''
#if the chosen partner is not a company and has a parent company, use the parent for the journal entries
#because you want to invoice 'Agrolait, accounting department' but the journal items are for 'Agrolait'
if part.parent_id and not part.is_company:
part = part.parent_id
return part
def copy(self, cr, uid, id, default=None, context=None): def copy(self, cr, uid, id, default=None, context=None):
default = default or {} default = default or {}
default.update({'invoice_ids' : []}) default.update({'invoice_ids' : []})

View File

@ -145,8 +145,7 @@
<header> <header>
<button name="invoice_open" states="draft,proforma2" string="Validate" class="oe_highlight" groups="account.group_account_invoice"/> <button name="invoice_open" states="draft,proforma2" string="Validate" class="oe_highlight" groups="account.group_account_invoice"/>
<button name="%(action_account_invoice_refund)d" type='action' string='Ask Refund' states='open,paid' groups="account.group_account_invoice"/> <button name="%(action_account_invoice_refund)d" type='action' string='Ask Refund' states='open,paid' groups="account.group_account_invoice"/>
<button name="invoice_cancel" states="draft,proforma2" string="Cancel" groups="account.group_account_invoice"/> <button name="invoice_cancel" states="draft,proforma2" string="Cancel Invoice" groups="account.group_account_invoice"/>
<button name="invoice_cancel" states="sale,open" string="Cancel" groups="base.group_no_one"/>
<button name="action_cancel_draft" states="cancel" string="Set to Draft" type="object" groups="account.group_account_invoice"/> <button name="action_cancel_draft" states="cancel" string="Set to Draft" type="object" groups="account.group_account_invoice"/>
<button name='%(action_account_state_open)d' type='action' string='Re-Open' groups="account.group_account_invoice" attrs="{'invisible':['|', ('state','&lt;&gt;','paid'), ('reconciled', '=', True)]}" help="This button only appears when the state of the invoice is 'paid' (showing that it has been fully reconciled) and auto-computed boolean 'reconciled' is False (depicting that it's not the case anymore). In other words, the invoice has been dereconciled and it does not fit anymore the 'paid' state. You should press this button to re-open it and let it continue its normal process after having resolved the eventual exceptions it may have created."/> <button name='%(action_account_state_open)d' type='action' string='Re-Open' groups="account.group_account_invoice" attrs="{'invisible':['|', ('state','&lt;&gt;','paid'), ('reconciled', '=', True)]}" help="This button only appears when the state of the invoice is 'paid' (showing that it has been fully reconciled) and auto-computed boolean 'reconciled' is False (depicting that it's not the case anymore). In other words, the invoice has been dereconciled and it does not fit anymore the 'paid' state. You should press this button to re-open it and let it continue its normal process after having resolved the eventual exceptions it may have created."/>
<field name="state" widget="statusbar" statusbar_visible="draft,open,paid" statusbar_colors='{"proforma":"blue","proforma2":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,open,paid" statusbar_colors='{"proforma":"blue","proforma2":"blue"}'/>
@ -300,7 +299,7 @@
<button name="invoice_open" states="proforma2" string="Validate" groups="base.group_user"/> <button name="invoice_open" states="proforma2" string="Validate" groups="base.group_user"/>
<button name="invoice_proforma2" states="draft" string="PRO-FORMA" groups="account.group_proforma_invoices"/> <button name="invoice_proforma2" states="draft" string="PRO-FORMA" groups="account.group_proforma_invoices"/>
<button name="%(action_account_invoice_refund)d" type='action' string='Refund Invoice' states='open,proforma2,paid' groups="base.group_user"/> <button name="%(action_account_invoice_refund)d" type='action' string='Refund Invoice' states='open,proforma2,paid' groups="base.group_user"/>
<button name="invoice_cancel" states="draft,proforma2,open" string="Cancel" groups="base.group_no_one"/> <button name="invoice_cancel" states="draft,proforma2,open" string="Cancel Invoice" groups="base.group_no_one"/>
<button name="action_cancel_draft" states="cancel" string="Reset to Draft" type="object" groups="base.group_user"/> <button name="action_cancel_draft" states="cancel" string="Reset to Draft" type="object" groups="base.group_user"/>
<button name='%(action_account_state_open)d' type='action' string='Re-Open' groups="account.group_account_invoice" attrs="{'invisible':['|', ('state','&lt;&gt;','paid'), ('reconciled', '=', True)]}" help="This button only appears when the state of the invoice is 'paid' (showing that it has been fully reconciled) and auto-computed boolean 'reconciled' is False (depicting that it's not the case anymore). In other words, the invoice has been dereconciled and it does not fit anymore the 'paid' state. You should press this button to re-open it and let it continue its normal process after having resolved the eventual exceptions it may have created."/> <button name='%(action_account_state_open)d' type='action' string='Re-Open' groups="account.group_account_invoice" attrs="{'invisible':['|', ('state','&lt;&gt;','paid'), ('reconciled', '=', True)]}" help="This button only appears when the state of the invoice is 'paid' (showing that it has been fully reconciled) and auto-computed boolean 'reconciled' is False (depicting that it's not the case anymore). In other words, the invoice has been dereconciled and it does not fit anymore the 'paid' state. You should press this button to re-open it and let it continue its normal process after having resolved the eventual exceptions it may have created."/>
<!--button name="%(account_invoices)d" string="Print Invoice" type="action" states="open,paid,proforma,sale,proforma2"/--> <!--button name="%(account_invoices)d" string="Print Invoice" type="action" states="open,paid,proforma,sale,proforma2"/-->
@ -437,7 +436,7 @@
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread" placeholder="Share a note..."/> <field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -448,11 +447,11 @@
<field name="model">account.invoice</field> <field name="model">account.invoice</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Invoice"> <search string="Search Invoice">
<field name="number" string="Invoice" filter_domain="['|','|', ('number','ilike',self), ('origin','ilike',self), ('supplier_invoice_number', 'ilike', self)]"/> <field name="number" string="Invoice" filter_domain="['|','|','|', ('number','ilike',self), ('origin','ilike',self), ('supplier_invoice_number', 'ilike', self), ('partner_id', 'ilike', self)]"/>
<filter name="draft" icon="terp-document-new" string="Draft" domain="[('state','=','draft')]" help="Draft Invoices"/> <filter name="draft" string="Draft" domain="[('state','=','draft')]" help="Draft Invoices"/>
<filter name="proforma" icon="terp-gtk-media-pause" string="Proforma" domain="[('state','=','proforma2')]" help="Proforma Invoices" groups="account.group_proforma_invoices"/> <filter name="proforma" string="Proforma" domain="[('state','=','proforma2')]" help="Proforma Invoices" groups="account.group_proforma_invoices"/>
<filter name="invoices" icon="terp-dolar" string="Invoices" domain="[('state','not in',['draft','cancel'])]" help="Proforma/Open/Paid Invoices"/> <filter name="invoices" string="Invoices" domain="[('state','not in',['draft','cancel'])]" help="Proforma/Open/Paid Invoices"/>
<filter name="unpaid" icon="terp-dolar_ok!" string="Unpaid" domain="[('state','=','open')]" help="Unpaid Invoices"/> <filter name="unpaid" string="Unpaid" domain="[('state','=','open')]" help="Unpaid Invoices"/>
<separator/> <separator/>
<filter domain="[('user_id','=',uid)]" help="My Invoices" icon="terp-personal"/> <filter domain="[('user_id','=',uid)]" help="My Invoices" icon="terp-personal"/>
<field name="partner_id"/> <field name="partner_id"/>

View File

@ -780,7 +780,7 @@ class account_move_line(osv.osv):
else: else:
currency_id = line.company_id.currency_id currency_id = line.company_id.currency_id
if line.reconcile_id: if line.reconcile_id:
raise osv.except_osv(_('Warning!'), _('Already reconciled.')) raise osv.except_osv(_('Warning'), _("Journal Item '%s' (id: %s), Move '%s' is already reconciled!") % (line.name, line.id, line.move_id.name))
if line.reconcile_partial_id: if line.reconcile_partial_id:
for line2 in line.reconcile_partial_id.line_partial_ids: for line2 in line.reconcile_partial_id.line_partial_ids:
if not line2.reconcile_id: if not line2.reconcile_id:

View File

@ -161,7 +161,7 @@
</p> </p>
</field> </field>
</record> </record>
<menuitem id="menu_action_account_period" action="action_account_period" parent="account.next_id_23" groups="base.group_no_one"/> <menuitem id="menu_action_account_period" action="action_account_period" parent="account.next_id_23"/>
<!-- Accounts --> <!-- Accounts -->
<record id="view_account_form" model="ir.ui.view"> <record id="view_account_form" model="ir.ui.view">
@ -435,15 +435,15 @@
<group string="Accounts"> <group string="Accounts">
<field name="profit_account_id" domain="[('type','!=','view')]"/> <field name="profit_account_id" domain="[('type','!=','view')]"/>
<field name="loss_account_id" domain="[('type','!=','view')]"/> <field name="loss_account_id" domain="[('type','!=','view')]"/>
<field name="internal_account_id"/> <field name="internal_account_id" domain="[('type','!=','view')]"/>
</group> </group>
<group string="Miscellaneous"> <group string="Miscellaneous">
<field name="with_last_closing_balance"/> <field name="with_last_closing_balance"/>
<field name="cash_control"/> <field name="cash_control" attrs="{'invisible':[('type','not in', ('cash',))]}"/>
</group> </group>
</group> </group>
<separator string="Available Coins" colspan="4" attrs="{'invisible' : [('cash_control', '=', False)] }"/> <separator string="Available Coins" colspan="4" attrs="{'invisible' : ['|',('cash_control', '=', False),('type','not in', ('cash',))] }"/>
<field name="cashbox_line_ids" nolabel="1" string="Unit Of Currency Definition" colspan="4" attrs="{'invisible' : [('cash_control', '=', False)]}"> <field name="cashbox_line_ids" nolabel="1" string="Unit Of Currency Definition" colspan="4" attrs="{'invisible' : ['|',('cash_control', '=', False),('type','not in', ('cash',))]}">
<tree string="CashBox Lines" editable="bottom"> <tree string="CashBox Lines" editable="bottom">
<field name="pieces" /> <field name="pieces" />
</tree> </tree>
@ -552,7 +552,7 @@
<header> <header>
<button name="button_confirm_bank" states="draft" string="Confirm" type="object" class="oe_highlight"/> <button name="button_confirm_bank" states="draft" string="Confirm" type="object" class="oe_highlight"/>
<button name="button_dummy" states="draft" string="Compute" type="object" class="oe_highlight"/> <button name="button_dummy" states="draft" string="Compute" type="object" class="oe_highlight"/>
<button name="button_cancel" states="confirm" string="Cancel" type="object"/> <button name="button_cancel" states="confirm" string="Cancel Statement" type="object"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/> <field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
</header> </header>
<sheet> <sheet>
@ -1206,6 +1206,8 @@
<field name="name"/> <field name="name"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="account_id"/> <field name="account_id"/>
<field name="period_id" invisible="1"/>
<field name="journal_id" invisible="1"/>
<field name="reconcile_partial_id"/> <field name="reconcile_partial_id"/>
<field name="state" invisible="1"/> <field name="state" invisible="1"/>
<field name="debit" sum="Total debit"/> <field name="debit" sum="Total debit"/>
@ -1256,7 +1258,7 @@
<form string="Account Entry" version="7.0"> <form string="Account Entry" version="7.0">
<header> <header>
<button name="button_validate" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/> <button name="button_validate" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/>
<button name="button_cancel" states="posted" string="Cancel" type="object" groups="account.group_account_invoice"/> <button name="button_cancel" states="posted" string="Cancel Entry" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar"/> <field name="state" widget="statusbar"/>
</header> </header>
<sheet string="Journal Entries" > <sheet string="Journal Entries" >
@ -2258,7 +2260,7 @@
<header> <header>
<button name="button_confirm_cash" states="open" string="Close CashBox" type="object" class="oe_highlight"/> <button name="button_confirm_cash" states="open" string="Close CashBox" type="object" class="oe_highlight"/>
<button name="button_open" states="draft" string="Open CashBox" type="object" class="oe_highlight"/> <button name="button_open" states="draft" string="Open CashBox" type="object" class="oe_highlight"/>
<button name="button_cancel" states="confirm,open" string="Cancel" type="object"/> <button name="button_cancel" states="confirm,open" string="Cancel CashBox" type="object"/>
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft,confirm"/> <field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft,confirm"/>
</header> </header>
<sheet string="Statement"> <sheet string="Statement">

View File

@ -95,6 +95,7 @@
</group> </group>
<field name="bank_ids" context="{'default_partner_id': active_id, 'form_view_ref': 'base.view_partner_bank_form'}"> <field name="bank_ids" context="{'default_partner_id': active_id, 'form_view_ref': 'base.view_partner_bank_form'}">
<tree string="Bank Details"> <tree string="Bank Details">
<field name="state" invisible="1"/>
<field name="sequence" invisible="1"/> <field name="sequence" invisible="1"/>
<field name="acc_number"/> <field name="acc_number"/>
<field name="bank_name"/> <field name="bank_name"/>

View File

@ -131,6 +131,7 @@
<field name="name"/> <field name="name"/>
<field name="account_id"/> <field name="account_id"/>
<field name="journal_id"/> <field name="journal_id"/>
<field name="user_id"/>
</group> </group>
<group> <group>
<field name="date"/> <field name="date"/>
@ -169,6 +170,7 @@
<field name="date"/> <field name="date"/>
<field name="ref" invisible="context.get('to_invoice', False)"/> <field name="ref" invisible="context.get('to_invoice', False)"/>
<field name="name"/> <field name="name"/>
<field name="user_id"/>
<field name="journal_id" invisible="context.get('to_invoice', False)"/> <field name="journal_id" invisible="context.get('to_invoice', False)"/>
<field name="amount" sum="Total" invisible="context.get('to_invoice', False)"/> <field name="amount" sum="Total" invisible="context.get('to_invoice', False)"/>
<field name="product_id" on_change="on_change_unit_amount(product_id, unit_amount, company_id, product_uom_id, journal_id)" invisible="not context.get('to_invoice', False)"/> <field name="product_id" on_change="on_change_unit_amount(product_id, unit_amount, company_id, product_uom_id, journal_id)" invisible="not context.get('to_invoice', False)"/>
@ -176,7 +178,6 @@
<field name="product_uom_id" on_change="on_change_unit_amount(product_id, unit_amount, company_id, product_uom_id)" invisible="not context.get('to_invoice', False)"/> <field name="product_uom_id" on_change="on_change_unit_amount(product_id, unit_amount, company_id, product_uom_id)" invisible="not context.get('to_invoice', False)"/>
<field domain="[('type','=','normal')]" name="account_id"/> <field domain="[('type','=','normal')]" name="account_id"/>
<field name="general_account_id" invisible="context.get('to_invoice', False)"/> <field name="general_account_id" invisible="context.get('to_invoice', False)"/>
<field name="user_id" invisible="1" />
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
</tree> </tree>
</field> </field>

View File

@ -65,7 +65,7 @@ class report_account_common(report_sxw.rml_parse, common_report_header):
vals['debit'] = report.debit vals['debit'] = report.debit
vals['credit'] = report.credit vals['credit'] = report.credit
if data['form']['enable_filter']: if data['form']['enable_filter']:
vals['balance_cmp'] = self.pool.get('account.financial.report').browse(self.cr, self.uid, report.id, context=data['form']['comparison_context']).balance vals['balance_cmp'] = self.pool.get('account.financial.report').browse(self.cr, self.uid, report.id, context=data['form']['comparison_context']).balance * report.sign or 0.0
lines.append(vals) lines.append(vals)
account_ids = [] account_ids = []
if report.display_detail == 'no_detail': if report.display_detail == 'no_detail':
@ -97,7 +97,7 @@ class report_account_common(report_sxw.rml_parse, common_report_header):
if not currency_obj.is_zero(self.cr, self.uid, account.company_id.currency_id, vals['balance']): if not currency_obj.is_zero(self.cr, self.uid, account.company_id.currency_id, vals['balance']):
flag = True flag = True
if data['form']['enable_filter']: if data['form']['enable_filter']:
vals['balance_cmp'] = account_obj.browse(self.cr, self.uid, account.id, context=data['form']['comparison_context']).balance vals['balance_cmp'] = account_obj.browse(self.cr, self.uid, account.id, context=data['form']['comparison_context']).balance * report.sign or 0.0
if not currency_obj.is_zero(self.cr, self.uid, account.company_id.currency_id, vals['balance_cmp']): if not currency_obj.is_zero(self.cr, self.uid, account.company_id.currency_id, vals['balance_cmp']):
flag = True flag = True
if flag: if flag:

View File

@ -714,6 +714,17 @@ class account_analytic_account(osv.osv):
self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context) self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
return True return True
def onchange_invoice_on_timesheets(self, cr, uid, ids, invoice_on_timesheets, context=None):
if not invoice_on_timesheets:
return {}
result = {'value': {'use_timesheets': True}}
try:
to_invoice = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'hr_timesheet_invoice', 'timesheet_invoice_factor1')
result['value']['to_invoice'] = to_invoice[1]
except ValueError:
pass
return result
class account_analytic_account_summary_user(osv.osv): class account_analytic_account_summary_user(osv.osv):
_name = "account_analytic_analysis.summary.user" _name = "account_analytic_analysis.summary.user"
_description = "Hours Summary by User" _description = "Hours Summary by User"

View File

@ -55,6 +55,19 @@ Hello ${object.name},
${account_table(ctx["data"]["future"].iteritems())} ${account_table(ctx["data"]["future"].iteritems())}
% endif % endif
<p>
You can check all contracts to be renewed using the menu:
</p>
<ul>
<li>Sales / Invoicing / Contracts to Renew</li>
</ul>
<p>
Thanks,
</p>
<pre>
--
OpenERP Automatic Email
</pre> </pre>
]]></field> ]]></field>

View File

@ -130,11 +130,11 @@
</tr> </tr>
</table> </table>
<group name='invoice_on_timesheets'> <group name='invoice_on_timesheets'>
<p class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"> <p name='invoice_on_timesheets_label' class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
When invoicing on timesheet, OpenERP uses the When reinvoicing costs, OpenERP uses the
pricelist of the contract which uses the price pricelist of the contract which uses the price
defined on the product related to each employee to defined on the product related (e.g timesheet
define the customer invoice price rate. products are defined on each employee).
</p> </p>
<group> <group>
<field name="pricelist_id" <field name="pricelist_id"
@ -218,7 +218,7 @@
<field name="parent_id"/> <field name="parent_id"/>
<filter name="open" string="In Progress" domain="[('state','in',('open','draft'))]" help="Contracts in progress (open, draft)"/> <filter name="open" string="In Progress" domain="[('state','in',('open','draft'))]" help="Contracts in progress (open, draft)"/>
<filter name="pending" string="To Renew" domain="[('state','=','pending')]" help="Pending contracts"/> <filter name="pending" string="To Renew" domain="[('state','=','pending')]" help="Pending contracts"/>
<filter name="closed" string="Closed" domain="[('state','=','pending')]" help="Closed contracts"/> <filter name="closed" string="Closed" domain="[('state','=','close')]" help="Closed contracts"/>
<filter name="cancelled" string="Cancelled" domain="[('state','=','cancel')]" help="Cancelled contracts"/> <filter name="cancelled" string="Cancelled" domain="[('state','=','cancel')]" help="Cancelled contracts"/>
<separator/> <separator/>
<filter <filter

View File

@ -308,8 +308,8 @@ class account_invoice_line(osv.osv):
res ['analytics_id'] = line.analytics_id and line.analytics_id.id or False res ['analytics_id'] = line.analytics_id and line.analytics_id.id or False
return res return res
def product_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None): def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):
res_prod = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context, company_id=company_id) res_prod = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom_id, qty, name, type, partner_id, fposition_id, price_unit, currency_id, context=context, company_id=company_id)
rec = self.pool.get('account.analytic.default').account_get(cr, uid, product, partner_id, uid, time.strftime('%Y-%m-%d'), context=context) rec = self.pool.get('account.analytic.default').account_get(cr, uid, product, partner_id, uid, time.strftime('%Y-%m-%d'), context=context)
if rec and rec.analytics_id: if rec and rec.analytics_id:
res_prod['value'].update({'analytics_id': rec.analytics_id.id}) res_prod['value'].update({'analytics_id': rec.analytics_id.id})

View File

@ -25,6 +25,7 @@ from dateutil.relativedelta import relativedelta
from openerp.osv import fields, osv from openerp.osv import fields, osv
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
from tools.translate import _
class account_asset_category(osv.osv): class account_asset_category(osv.osv):
_name = 'account.asset.category' _name = 'account.asset.category'
@ -75,6 +76,12 @@ class account_asset_asset(osv.osv):
_name = 'account.asset.asset' _name = 'account.asset.asset'
_description = 'Asset' _description = 'Asset'
def unlink(self, cr, uid, ids, context=None):
for asset in self.browse(cr, uid, ids, context=context):
if asset.account_move_line_ids:
raise osv.except_osv(_('Error!'), _('You cannot delete an asset that contains posted depreciation lines.'))
return super(account_account, self).unlink(cr, uid, ids, context=context)
def _get_period(self, cr, uid, context=None): def _get_period(self, cr, uid, context=None):
periods = self.pool.get('account.period').find(cr, uid) periods = self.pool.get('account.period').find(cr, uid)
if periods: if periods:
@ -369,7 +376,7 @@ class account_asset_depreciation_line(osv.osv):
_columns = { _columns = {
'name': fields.char('Depreciation Name', size=64, required=True, select=1), 'name': fields.char('Depreciation Name', size=64, required=True, select=1),
'sequence': fields.integer('Sequence', required=True), 'sequence': fields.integer('Sequence', required=True),
'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True), 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True, ondelete='cascade'),
'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'), 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'),
'amount': fields.float('Current Depreciation', digits_compute=dp.get_precision('Account'), required=True), 'amount': fields.float('Current Depreciation', digits_compute=dp.get_precision('Account'), required=True),
'remaining_value': fields.float('Next Period Depreciation', digits_compute=dp.get_precision('Account'),required=True), 'remaining_value': fields.float('Next Period Depreciation', digits_compute=dp.get_precision('Account'),required=True),
@ -454,7 +461,7 @@ account_asset_depreciation_line()
class account_move_line(osv.osv): class account_move_line(osv.osv):
_inherit = 'account.move.line' _inherit = 'account.move.line'
_columns = { _columns = {
'asset_id': fields.many2one('account.asset.asset', 'Asset'), 'asset_id': fields.many2one('account.asset.asset', 'Asset', ondelete="restrict"),
'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}), 'entry_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}),
} }

View File

@ -99,7 +99,7 @@
<button string="Approve" name="validate" states="confirm" type="workflow" class="oe_highlight"/> <button string="Approve" name="validate" states="confirm" type="workflow" class="oe_highlight"/>
<button string="Done" name="done" states="validate" type="workflow" class="oe_highlight"/> <button string="Done" name="done" states="validate" type="workflow" class="oe_highlight"/>
<button name="draft" states="cancel" string="Reset to Draft" type="workflow" /> <button name="draft" states="cancel" string="Reset to Draft" type="workflow" />
<button string="Cancel" name="cancel" states="confirm,validate" type="workflow"/> <button string="Cancel Budget" name="cancel" states="confirm,validate" type="workflow"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/> <field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
</header> </header>
<sheet string="Budget"> <sheet string="Budget">

View File

@ -18,7 +18,7 @@
<field name="inherit_id" ref="account.invoice_form"/> <field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//button[@name='invoice_cancel']" position="replace"> <xpath expr="//button[@name='invoice_cancel']" position="replace">
<button name="invoice_cancel" states="draft,proforma2,sale,open" string="Cancel" groups="account.group_account_invoice"/> <button name="invoice_cancel" states="draft,proforma2,sale,open" string="Cancel Invoice" groups="account.group_account_invoice"/>
</xpath> </xpath>
</field> </field>
</record> </record>
@ -29,7 +29,7 @@
<field name="inherit_id" ref="account.invoice_supplier_form"/> <field name="inherit_id" ref="account.invoice_supplier_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//button[@name='invoice_cancel']" position="replace"> <xpath expr="//button[@name='invoice_cancel']" position="replace">
<button name="invoice_cancel" states="draft,proforma2,sale,open" string="Cancel" groups="account.group_account_invoice"/> <button name="invoice_cancel" states="draft,proforma2,sale,open" string="Cancel Invoice" groups="account.group_account_invoice"/>
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@ -292,8 +292,7 @@ class res_partner(osv.osv):
type = 'comment', type = 'comment',
subtype = "mail.mt_comment", context = context, subtype = "mail.mt_comment", context = context,
model = 'res.partner', res_id = part.id, model = 'res.partner', res_id = part.id,
notified_partner_ids = [(6, 0, [responsible_partner_id])], partner_ids = [responsible_partner_id])
partner_ids = [(6, 0, [responsible_partner_id])])
return super(res_partner, self).write(cr, uid, ids, vals, context=context) return super(res_partner, self).write(cr, uid, ids, vals, context=context)
def action_done(self, cr, uid, ids, context=None): def action_done(self, cr, uid, ids, context=None):

View File

@ -24,6 +24,17 @@
</field> </field>
</record> </record>
<record id="view_partner_inherit_customer_followup_tree" model="ir.ui.view">
<field name="name">res.partner.followup.inherit.tree</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="payment_responsible_id" invisible="1"/>
</field>
</field>
</record>
<record id="customer_followup_search_view" model="ir.ui.view"> <record id="customer_followup_search_view" model="ir.ui.view">
<field name="name">Search</field> <field name="name">Search</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>

View File

@ -52,10 +52,10 @@ class TestAccountFollowup(TransactionCase):
def test_00_send_followup_after_3_days(self): def test_00_send_followup_after_3_days(self):
""" Send follow up after 3 days and check nothing is done (as first follow-up level is only after 15 days)""" """ Send follow up after 3 days and check nothing is done (as first follow-up level is only after 15 days)"""
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
current_date = datetime.datetime.now() current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=3) delta = datetime.timedelta(days=3)
result = current_date + delta result = current_date + delta
self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id 'followup_id': self.followup_id
}, context={"followup_id": self.followup_id}) }, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})
@ -64,34 +64,33 @@ class TestAccountFollowup(TransactionCase):
def run_wizard_three_times(self): def run_wizard_three_times(self):
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
current_date = datetime.datetime.now() current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=40) delta = datetime.timedelta(days=40)
result = current_date + delta result = current_date + delta
self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id 'followup_id': self.followup_id
}, context={"followup_id": self.followup_id}) }, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id 'followup_id': self.followup_id
}, context={"followup_id": self.followup_id}) }, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id 'followup_id': self.followup_id,
}, context={"followup_id": self.followup_id}) }, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
def test_01_send_followup_later_for_upgrade(self): def test_01_send_followup_later_for_upgrade(self):
""" Send one follow-up after 15 days to check it upgrades to level 1""" """ Send one follow-up after 15 days to check it upgrades to level 1"""
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
current_date = datetime.datetime.now() current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=15) delta = datetime.timedelta(days=15)
result = current_date + delta result = current_date + delta
self.wizard_id = self.wizard.create(cr, uid, { self.wizard_id = self.wizard.create(cr, uid, {
'date':result.strftime("%Y-%m-%d"), 'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id 'followup_id': self.followup_id
}, context={"followup_id": self.followup_id}) }, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id, 'tz':'UTC'})
self.assertEqual(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id.id, self.first_followup_line_id, self.assertEqual(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id.id, self.first_followup_line_id,
"Not updated to the correct follow-up level") "Not updated to the correct follow-up level")
@ -102,12 +101,12 @@ class TestAccountFollowup(TransactionCase):
self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action, self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action,
"Call the customer on the phone! ", "Manual action not set") "Call the customer on the phone! ", "Manual action not set")
self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action_date, self.assertEqual(self.partner.browse(cr, uid, self.partner_id).payment_next_action_date,
datetime.datetime.now().strftime("%Y-%m-%d")) datetime.datetime.utcnow().strftime(tools.DEFAULT_SERVER_DATE_FORMAT))
def test_03_filter_on_credit(self): def test_03_filter_on_credit(self):
""" Check the partners can be filtered on having credits """ """ Check the partners can be filtered on having credits """
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
ids = self.partner.search(cr, uid, [('payment_amount_due', '>=', 0.0)]) ids = self.partner.search(cr, uid, [('payment_amount_due', '>', 0.0)])
self.assertIn(self.partner_id, ids) self.assertIn(self.partner_id, ids)
def test_04_action_done(self): def test_04_action_done(self):
@ -139,7 +138,7 @@ class TestAccountFollowup(TransactionCase):
"""Run wizard until manual action, pay the invoice and check that partner has no follow-up level anymore and after running the wizard the action is empty""" """Run wizard until manual action, pay the invoice and check that partner has no follow-up level anymore and after running the wizard the action is empty"""
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
self.test_02_check_manual_action() self.test_02_check_manual_action()
current_date = datetime.datetime.now() current_date = datetime.datetime.utcnow()
delta = datetime.timedelta(days=1) delta = datetime.timedelta(days=1)
result = current_date + delta result = current_date + delta
self.invoice.pay_and_reconcile(cr, uid, [self.invoice_id], 1000.0, self.pay_account_id, self.invoice.pay_and_reconcile(cr, uid, [self.invoice_id], 1000.0, self.pay_account_id,
@ -147,7 +146,7 @@ class TestAccountFollowup(TransactionCase):
self.period_id, self.journal_id, self.period_id, self.journal_id,
name = "Payment for test customer invoice follow-up") name = "Payment for test customer invoice follow-up")
self.assertFalse(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id, "Level not empty") self.assertFalse(self.partner.browse(cr, uid, self.partner_id).latest_followup_level_id, "Level not empty")
self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime("%Y-%m-%d"), self.wizard_id = self.wizard.create(cr, uid, {'date':result.strftime(tools.DEFAULT_SERVER_DATE_FORMAT),
'followup_id': self.followup_id 'followup_id': self.followup_id
}, context={"followup_id": self.followup_id}) }, context={"followup_id": self.followup_id})
self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id}) self.wizard.do_process(cr, uid, [self.wizard_id], context={"followup_id": self.followup_id})

View File

@ -88,7 +88,7 @@
<button name="open" states="draft" string="Confirm Payments" class="oe_highlight"/> <button name="open" states="draft" string="Confirm Payments" class="oe_highlight"/>
<button name="set_done" states="open" string="Make Payments" type="object" class="oe_highlight"/> <button name="set_done" states="open" string="Make Payments" type="object" class="oe_highlight"/>
<button name="set_to_draft" states="cancel" string="Set to draft" type="object"/> <button name="set_to_draft" states="cancel" string="Set to draft" type="object"/>
<button name="cancel" states="draft,open" string="Cancel"/> <button name="cancel" states="draft,open" string="Cancel Payments"/>
<field name="state" widget="statusbar" statusbar_visible="draft,open"/> <field name="state" widget="statusbar" statusbar_visible="draft,open"/>
</header> </header>
<sheet string="Payment order"> <sheet string="Payment order">

View File

@ -43,8 +43,8 @@
<form string="Accounting Voucher" version="7.0"> <form string="Accounting Voucher" version="7.0">
<header> <header>
<button name="proforma_voucher" string="Post" states="draft" class="oe_highlight"/> <button name="proforma_voucher" string="Post" states="draft" class="oe_highlight"/>
<button name="cancel_voucher" string="Cancel" type="object" states="posted" confirm="Are you sure to unreconcile this record?"/> <button name="cancel_voucher" string="Cancel Voucher" type="object" states="posted" confirm="Are you sure you want to unreconcile this record?"/>
<button name="cancel_voucher" string="Cancel" states="draft,proforma" /> <button name="cancel_voucher" string="Cancel Voucher" states="draft,proforma" />
<button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/> <button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/>
</header> </header>

View File

@ -41,7 +41,7 @@ class invoice(osv.osv):
'target': 'new', 'target': 'new',
'domain': '[]', 'domain': '[]',
'context': { 'context': {
'default_partner_id': self._find_partner(inv).id, 'default_partner_id': self.pool.get('res.partner')._find_accounting_partner(inv.partner_id).id,
'default_amount': inv.type in ('out_refund', 'in_refund') and -inv.residual or inv.residual, 'default_amount': inv.type in ('out_refund', 'in_refund') and -inv.residual or inv.residual,
'default_reference': inv.name, 'default_reference': inv.name,
'close_after_process': True, 'close_after_process': True,

View File

@ -126,7 +126,7 @@
<form string="Voucher Payment" version="7.0"> <form string="Voucher Payment" version="7.0">
<header> <header>
<button name="proforma_voucher" string="Validate" states="draft" invisible="context.get('line_type', False)" class="oe_highlight"/> <button name="proforma_voucher" string="Validate" states="draft" invisible="context.get('line_type', False)" class="oe_highlight"/>
<button name="cancel_voucher" string="Cancel" states="draft,proforma" invisible="context.get('line_type', False)"/> <button name="cancel_voucher" string="Cancel Voucher" states="draft,proforma" invisible="context.get('line_type', False)"/>
<button name="cancel_voucher" string="Unreconcile" type="object" states="posted" invisible="context.get('line_type', False)" confirm="Are you sure to unreconcile and cancel this record ?"/> <button name="cancel_voucher" string="Unreconcile" type="object" states="posted" invisible="context.get('line_type', False)" confirm="Are you sure to unreconcile and cancel this record ?"/>
<button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft" invisible="context.get('line_type', False)"/> <button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft" invisible="context.get('line_type', False)"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/>
@ -394,7 +394,7 @@
<form string="Receipt" version="7.0"> <form string="Receipt" version="7.0">
<header invisible="context.get('line_type', False)"> <header invisible="context.get('line_type', False)">
<button name="proforma_voucher" string="Validate" states="draft" class="oe_highlight"/> <button name="proforma_voucher" string="Validate" states="draft" class="oe_highlight"/>
<button name="cancel_voucher" string="Cancel" states="draft,proforma"/> <button name="cancel_voucher" string="Cancel Receipt" states="draft,proforma"/>
<button name="cancel_voucher" string="Unreconcile" type="object" states="posted" confirm="Are you sure to unreconcile and cancel this record ?"/> <button name="cancel_voucher" string="Unreconcile" type="object" states="posted" confirm="Are you sure to unreconcile and cancel this record ?"/>
<button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/> <button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/>

View File

@ -59,17 +59,17 @@
<field name="name">account.voucher.sale.form</field> <field name="name">account.voucher.sale.form</field>
<field name="model">account.voucher</field> <field name="model">account.voucher</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Sale voucher" version="7.0"> <form string="Sales Receipt" version="7.0">
<header> <header>
<button name="proforma_voucher" string="Validate" states="draft" class="oe_highlight"/> <button name="proforma_voucher" string="Validate" states="draft" class="oe_highlight"/>
<button name="%(act_pay_voucher)d" context="{'narration':narration, 'title':'Customer Payment', 'type':'receipt', 'partner_id':partner_id, 'reference':reference, 'amount':amount}" type="action" string="Validate Payment" attrs="{'invisible':['|',('pay_now','=','pay_now'),'|',('state','=','draft'), ('paid','=',True)]}" class="oe_highlight"/> <button name="%(act_pay_voucher)d" context="{'narration':narration, 'title':'Customer Payment', 'type':'receipt', 'partner_id':partner_id, 'reference':reference, 'amount':amount}" type="action" string="Validate Payment" attrs="{'invisible':['|',('pay_now','=','pay_now'),'|',('state','=','draft'), ('paid','=',True)]}" class="oe_highlight"/>
<button name="cancel_voucher" string="Cancel" states="draft,proforma" /> <button name="cancel_voucher" string="Cancel Receipt" states="draft,proforma" />
<button name="cancel_voucher" string="Cancel" type="object" states="posted" confirm="Are you sure you want to cancel this receipt?"/> <button name="cancel_voucher" string="Cancel Receipt" type="object" states="posted" confirm="Are you sure you want to cancel this receipt?"/>
<button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/> <button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/>
</header> </header>
<sheet string="Sales Receipts" > <sheet string="Sales Receipt" >
<h1><label for="number" string="Sale Receipt"/> <field name="number" class="oe_inline" readonly="1"/></h1> <h1><label for="number" string="Sales Receipt"/> <field name="number" class="oe_inline" readonly="1"/></h1>
<group> <group>
<group> <group>
<field name="type" invisible="True"/> <field name="type" invisible="True"/>
@ -208,8 +208,8 @@
<header> <header>
<button name="proforma_voucher" string="Validate" states="draft" class="oe_highlight"/> <button name="proforma_voucher" string="Validate" states="draft" class="oe_highlight"/>
<button name="%(act_pay_bills)d" context="{'narration':narration, 'title':'Bill Payment', 'type':'payment', 'partner_id': partner_id, 'reference':reference}" type="action" string="Pay Bill" attrs="{'invisible':['|',('pay_now','=','pay_now'),'|',('state','=','draft'), ('paid','=',True)]}" class="oe_highlight"/> <button name="%(act_pay_bills)d" context="{'narration':narration, 'title':'Bill Payment', 'type':'payment', 'partner_id': partner_id, 'reference':reference}" type="action" string="Pay Bill" attrs="{'invisible':['|',('pay_now','=','pay_now'),'|',('state','=','draft'), ('paid','=',True)]}" class="oe_highlight"/>
<button name="cancel_voucher" string="Cancel" states="draft,proforma" /> <button name="cancel_voucher" string="Cancel Voucher" states="draft,proforma" />
<button name="cancel_voucher" string="Cancel" type="object" states="posted" confirm="Are you sure you want to cancel this receipt?"/> <button name="cancel_voucher" string="Cancel Voucher" type="object" states="posted" confirm="Are you sure you want to cancel this receipt?"/>
<button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/> <button name="action_cancel_draft" type="object" states="cancel" string="Set to Draft"/>
<field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,posted" statusbar_colors='{"proforma":"blue"}'/>
</header> </header>

View File

@ -40,6 +40,16 @@
</td> </td>
</tr> </tr>
</xpath> </xpath>
<xpath expr="//p[@name='invoice_on_timesheets_label']" position="attributes">
<attribute name="attrs">{'invisible': [('invoice_on_timesheets','=',False),('charge_expenses','=',False)]}</attribute>
</xpath>
<xpath expr="//field[@name='pricelist_id']" position="attributes">
<attribute name="attrs">{'required': ['|',('invoice_on_timesheets','=',True),('charge_expenses','=',True)], 'invisible':[('invoice_on_timesheets','=',False), ('charge_expenses','=',False)]}</attribute>
</xpath>
<xpath expr="//field[@name='to_invoice']" position="attributes">
<attribute name="attrs">{'required': ['|',('invoice_on_timesheets','=',True),('charge_expenses','=',True)]}</attribute>
<attribute name="string">Expenses and Timesheet Invoicing Ratio</attribute>
</xpath>
</field> </field>
</record> </record>

View File

@ -42,4 +42,5 @@ Allow users to sign up and reset their password
], ],
'js': ['static/src/js/auth_signup.js'], 'js': ['static/src/js/auth_signup.js'],
'qweb': ['static/src/xml/auth_signup.xml'], 'qweb': ['static/src/xml/auth_signup.xml'],
'bootstrap': True,
} }

View File

@ -19,9 +19,6 @@
# #
############################################################################## ##############################################################################
import logging import logging
import urllib
import werkzeug
import openerp import openerp
from openerp.modules.registry import RegistryManager from openerp.modules.registry import RegistryManager
@ -54,9 +51,8 @@ class Controller(openerp.addons.web.http.Controller):
return user_info return user_info
@openerp.addons.web.http.jsonrequest @openerp.addons.web.http.jsonrequest
def signup(self, req, dbname, token, name, login, password): def signup(self, req, dbname, token, **values):
""" sign up a user (new or existing)""" """ sign up a user (new or existing)"""
values = {'name': name, 'login': login, 'password': password}
try: try:
self._signup_with_values(req, dbname, token, values) self._signup_with_values(req, dbname, token, values)
except SignupError, e: except SignupError, e:
@ -69,7 +65,7 @@ class Controller(openerp.addons.web.http.Controller):
res_users = registry.get('res.users') res_users = registry.get('res.users')
res_users.signup(cr, openerp.SUPERUSER_ID, values, token) res_users.signup(cr, openerp.SUPERUSER_ID, values, token)
@openerp.addons.web.http.httprequest @openerp.addons.web.http.jsonrequest
def reset_password(self, req, dbname, login): def reset_password(self, req, dbname, login):
""" retrieve user, and perform reset password """ """ retrieve user, and perform reset password """
registry = RegistryManager.get(dbname) registry = RegistryManager.get(dbname)
@ -78,12 +74,10 @@ class Controller(openerp.addons.web.http.Controller):
res_users = registry.get('res.users') res_users = registry.get('res.users')
res_users.reset_password(cr, openerp.SUPERUSER_ID, login) res_users.reset_password(cr, openerp.SUPERUSER_ID, login)
cr.commit() cr.commit()
message = 'An email has been sent with credentials to reset your password'
except Exception as e: except Exception as e:
# signup error # signup error
_logger.exception('error when resetting password') _logger.exception('error when resetting password')
message = e.message raise(e)
params = [('action', 'login'), ('error_message', message)] return True
return werkzeug.utils.redirect("/#" + urllib.urlencode(params))
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -5,32 +5,33 @@ openerp.auth_signup = function(instance) {
instance.web.Login.include({ instance.web.Login.include({
start: function() { start: function() {
var self = this; var self = this;
var d = this._super(); this.signup_enabled = false;
d.done(function() { this.reset_password_enabled = false;
self.$(".oe_signup_show").hide(); return this._super().then(function() {
// Switches the login box to the select mode whith mode == [default|signup|reset]
self.on('change:login_mode', self, function() {
var mode = self.get('login_mode') || 'default';
self.$('*[data-modes]').each(function() {
var modes = $(this).data('modes').split(/\s+/);
$(this).toggle(modes.indexOf(mode) > -1);
});
self.$('a.oe_signup_signup:visible').toggle(self.signup_enabled);
self.$('a.oe_signup_reset_password:visible').toggle(self.reset_password_enabled);
});
// to switch between the signup and regular login form // to switch between the signup and regular login form
self.$('a.oe_signup_signup').click(function(ev) { self.$('a.oe_signup_signup').click(function(ev) {
if (ev) { self.set('login_mode', 'signup');
ev.preventDefault();
}
self.$el.addClass("oe_login_signup");
self.$(".oe_signup_show").show();
self.$(".oe_signup_hide").hide();
return false; return false;
}); });
self.$('a.oe_signup_back').click(function(ev) { self.$('a.oe_signup_back').click(function(ev) {
if (ev) { self.set('login_mode', 'default');
ev.preventDefault();
}
self.$el.removeClass("oe_login_signup");
self.$(".oe_signup_show").hide();
self.$(".oe_signup_hide").show();
delete self.params.token; delete self.params.token;
return false; return false;
}); });
var dblist = self.db_list || []; var dbname = self.selected_db;
var dbname = self.params.db || (dblist.length === 1 ? dblist[0] : null);
// if there is an error message in params, show it then forget it // if there is an error message in params, show it then forget it
if (self.params.error_message) { if (self.params.error_message) {
@ -42,7 +43,7 @@ openerp.auth_signup = function(instance) {
if (dbname && self.params.token) { if (dbname && self.params.token) {
self.rpc("/auth_signup/retrieve", {dbname: dbname, token: self.params.token}) self.rpc("/auth_signup/retrieve", {dbname: dbname, token: self.params.token})
.done(self.on_token_loaded) .done(self.on_token_loaded)
.fail(self.on_token_failed) .fail(self.on_token_failed);
} }
if (dbname && self.params.login) { if (dbname && self.params.login) {
self.$("form input[name=login]").val(self.params.login); self.$("form input[name=login]").val(self.params.login);
@ -51,23 +52,21 @@ openerp.auth_signup = function(instance) {
// bind reset password link // bind reset password link
self.$('a.oe_signup_reset_password').click(self.do_reset_password); self.$('a.oe_signup_reset_password').click(self.do_reset_password);
// make signup link and reset password link visible only when enabled
self.$('a.oe_signup_signup').hide();
self.$('a.oe_signup_reset_password').hide();
if (dbname) { if (dbname) {
self.rpc("/auth_signup/get_config", {dbname: dbname}) self.rpc("/auth_signup/get_config", {dbname: dbname}).done(function(result) {
.done(function(result) { self.signup_enabled = result.signup;
if (result.signup) { self.reset_password_enabled = result.reset_password;
self.$('a.oe_signup_signup').show(); if (self.$("form input[name=login]").val()){
} self.set('login_mode', 'default');
if (result.reset_password) { } else {
self.$('a.oe_signup_reset_password').show(); self.set('login_mode', 'signup');
} }
}); });
} else {
// TODO: support multiple database mode
self.set('login_mode', 'default');
} }
}); });
return d;
}, },
on_token_loaded: function(result) { on_token_loaded: function(result) {
@ -76,9 +75,7 @@ openerp.auth_signup = function(instance) {
this.on_db_loaded([result.db]); this.on_db_loaded([result.db]);
if (result.token) { if (result.token) {
// switch to signup mode, set user name and login // switch to signup mode, set user name and login
this.$el.addClass("oe_login_signup"); this.set('login_mode', (this.params.type === 'reset' ? 'reset' : 'signup'));
self.$(".oe_signup_show").show();
self.$(".oe_signup_hide").hide();
this.$("form input[name=name]").val(result.name).attr("readonly", "readonly"); this.$("form input[name=name]").val(result.name).attr("readonly", "readonly");
if (result.login) { if (result.login) {
this.$("form input[name=login]").val(result.login).attr("readonly", "readonly"); this.$("form input[name=login]").val(result.login).attr("readonly", "readonly");
@ -88,6 +85,7 @@ openerp.auth_signup = function(instance) {
} else { } else {
// remain in login mode, set login if present // remain in login mode, set login if present
delete this.params.token; delete this.params.token;
this.set('login_mode', 'default');
this.$("form input[name=login]").val(result.login || ""); this.$("form input[name=login]").val(result.login || "");
} }
}, },
@ -99,43 +97,52 @@ openerp.auth_signup = function(instance) {
this.show_error(_t("Invalid signup token")); this.show_error(_t("Invalid signup token"));
delete this.params.db; delete this.params.db;
delete this.params.token; delete this.params.token;
this.set('login_mode', 'default');
},
get_params: function(){
// signup user (or reset password)
var db = this.$("form [name=db]").val();
var name = this.$("form input[name=name]").val();
var login = this.$("form input[name=login]").val();
var password = this.$("form input[name=password]").val();
var confirm_password = this.$("form input[name=confirm_password]").val();
if (!db) {
this.do_warn(_t("Login"), _t("No database selected !"));
return false;
} else if (!name) {
this.do_warn(_t("Login"), _t("Please enter a name."));
return false;
} else if (!login) {
this.do_warn(_t("Login"), _t("Please enter a username."));
return false;
} else if (!password || !confirm_password) {
this.do_warn(_t("Login"), _t("Please enter a password and confirm it."));
return false;
} else if (password !== confirm_password) {
this.do_warn(_t("Login"), _t("Passwords do not match; please retype them."));
return false;
}
var params = {
dbname : db,
token: this.params.token || "",
name: name,
login: login,
password: password,
};
return params;
}, },
on_submit: function(ev) { on_submit: function(ev) {
if (ev) { if (ev) {
ev.preventDefault(); ev.preventDefault();
} }
if (this.$el.hasClass("oe_login_signup")) { var login_mode = this.get('login_mode');
// signup user (or reset password) if (login_mode === 'signup' || login_mode === 'reset') {
var db = this.$("form [name=db]").val(); var params = this.get_params();
var name = this.$("form input[name=name]").val(); if (_.isEmpty(params)){
var login = this.$("form input[name=login]").val();
var password = this.$("form input[name=password]").val();
var confirm_password = this.$("form input[name=confirm_password]").val();
if (!db) {
this.do_warn(_t("Login"), _t("No database selected !"));
return false;
} else if (!name) {
this.do_warn(_t("Login"), _t("Please enter a name."));
return false;
} else if (!login) {
this.do_warn(_t("Login"), _t("Please enter a username."));
return false;
} else if (!password || !confirm_password) {
this.do_warn(_t("Login"), _t("Please enter a password and confirm it."));
return false;
} else if (password !== confirm_password) {
this.do_warn(_t("Login"), _t("Passwords do not match; please retype them."));
return false; return false;
} }
var params = {
dbname : db,
token: this.params.token || "",
name: name,
login: login,
password: password,
};
var self = this, var self = this,
super_ = this._super; super_ = this._super;
this.rpc('/auth_signup/signup', params) this.rpc('/auth_signup/signup', params)
@ -156,21 +163,23 @@ openerp.auth_signup = function(instance) {
if (ev) { if (ev) {
ev.preventDefault(); ev.preventDefault();
} }
var self = this;
var db = this.$("form [name=db]").val(); var db = this.$("form [name=db]").val();
var login = this.$("form input[name=login]").val(); var login = this.$("form input[name=login]").val();
if (!db) { if (!db) {
this.do_warn(_t("Login"), _t("No database selected !")); this.do_warn(_t("Login"), _t("No database selected !"));
return false; return $.Deferred().reject();
} else if (!login) { } else if (!login) {
this.do_warn(_t("Login"), _t("Please enter a username or email address.")) this.do_warn(_t("Login"), _t("Please enter a username or email address."));
return false; return $.Deferred().reject();
} }
var params = { return self.rpc("/auth_signup/reset_password", { dbname: db, login: login }).done(function(result) {
dbname : db, self.show_error(_t("An email has been sent with credentials to reset your password"));
login: login, self.set('login_mode', 'default');
}; }).fail(function(result, ev) {
var url = "/auth_signup/reset_password?" + $.param(params); ev.preventDefault();
window.location = url; self.show_error(result.message);
});
}, },
}); });
}; };

View File

@ -5,26 +5,32 @@
<t t-extend="Login"> <t t-extend="Login">
<t t-jquery="form ul:first li:contains('Username')" t-operation="before"> <t t-jquery="form ul:first li:contains('Username')" t-operation="before">
<li class="oe_signup_show">Name</li> <li data-modes="signup reset">Name</li>
<li class="oe_signup_show"><input name="name" type="text"/></li> <li data-modes="signup reset"><input name="name" type="text"/></li>
</t> </t>
<t t-jquery="form ul:first li:contains('Username')" t-operation="replace"> <t t-jquery="form ul:first li:contains('Username')" t-operation="replace">
<li class="oe_signup_hide">Username</li> <li data-modes="default">Username</li>
<li class="oe_signup_show">Username (Email)</li> <li data-modes="signup reset">Username (Email)</li>
</t> </t>
<t t-jquery="form ul:first li:has(input[name=password])" t-operation="after"> <t t-jquery="form ul:first li:has(input[name=password])" t-operation="after">
<li class="oe_signup_show">Confirm Password</li> <li data-modes="signup reset">Confirm Password</li>
<li class="oe_signup_show"><input name="confirm_password" type="password"/></li> <li data-modes="signup reset"><input name="confirm_password" type="password"/></li>
</t> </t>
<t t-jquery="form ul:first li:has(button[name=submit])" t-operation="replace"> <t t-jquery="form ul:first li:has(button[name=submit])" t-operation="replace">
<li> <li>
<button class="oe_signup_hide" name="submit">Log in</button> <button name="submit">
<button class="oe_signup_show" name="submit">Sign up</button> <span data-modes="default">Log in</span>
<a class="oe_signup_hide oe_signup_signup" href="#">Sign Up</a> <span data-modes="signup">Sign Up</span>
<a class="oe_signup_show oe_signup_back" href="#">Back to Login</a> <span data-modes="reset">Reset password</span>
<a class="oe_signup_reset_password" href="#">Reset password</a> </button>
<a class="oe_signup_signup" data-modes="default" href="#">Sign Up</a>
<a class="oe_signup_back" data-modes="signup reset" href="#">Back to Login</a>
<a class="oe_signup_reset_password" data-modes="default" href="#">Reset password</a>
</li> </li>
</t> </t>
<t t-jquery=".oe_login_manage_db">
this.attr('data-modes', 'default');
</t>
</t> </t>
</templates> </templates>

View File

@ -214,6 +214,13 @@ class base_action_rule(osv.osv):
self._register_hook(cr, ids) self._register_hook(cr, ids)
return True return True
def onchange_model_id(self, cr, uid, ids, model_id, context=None):
data = {'model': False, 'filter_pre_id': False, 'filter_id': False}
if model_id:
model = self.pool.get('ir.model').browse(cr, uid, model_id, context=context)
data.update({'model': model.model})
return {'value': data}
def _check(self, cr, uid, automatic=False, use_new_cursor=False, context=None): def _check(self, cr, uid, automatic=False, use_new_cursor=False, context=None):
""" This Function is called by scheduler. """ """ This Function is called by scheduler. """
context = context or {} context = context or {}

View File

@ -15,7 +15,7 @@
<h1><field name="name"/></h1> <h1><field name="name"/></h1>
<group> <group>
<group> <group>
<field name="model_id"/> <field name="model_id" on_change="onchange_model_id(model_id, context)"/>
<field name="model" invisible="1"/> <field name="model" invisible="1"/>
</group> </group>
<group> <group>

View File

@ -70,7 +70,7 @@
<header> <header>
<button name="do_confirm" string="Confirm" states="tentative,cancelled" type="object" class="oe_highlight"/> <button name="do_confirm" string="Confirm" states="tentative,cancelled" type="object" class="oe_highlight"/>
<button name="do_tentative" states="confirmed,cancelled" string="Uncertain" type="object" class="oe_highlight"/> <button name="do_tentative" states="confirmed,cancelled" string="Uncertain" type="object" class="oe_highlight"/>
<button name="do_cancel" string="Cancel" states="tentative,confirmed" type="object"/> <button name="do_cancel" string="Cancel Event" states="tentative,confirmed" type="object"/>
<field name="state" widget="statusbar" <field name="state" widget="statusbar"
statusbar_visible="tentative,confirmed" statusbar_colors='{"proforma":"blue"}'/> statusbar_visible="tentative,confirmed" statusbar_colors='{"proforma":"blue"}'/>
</header> </header>

View File

@ -19,10 +19,11 @@
# #
############################################################################## ##############################################################################
import base64
import openerp.modules.registry
from openerp.osv import osv from openerp.osv import osv
from openerp_sxw2rml import sxw2rml from openerp_sxw2rml import sxw2rml
from StringIO import StringIO from StringIO import StringIO
import base64
from openerp import pooler from openerp import pooler
from openerp import addons from openerp import addons
@ -55,7 +56,12 @@ class report_xml(osv.osv):
'report_sxw_content': base64.decodestring(file_sxw), 'report_sxw_content': base64.decodestring(file_sxw),
'report_rml_content': str(sxw2rml(sxwval, xsl=fp.read())), 'report_rml_content': str(sxw2rml(sxwval, xsl=fp.read())),
}) })
# FIXME: this should be moved to an override of the ir.actions.report_xml.create() method
cr.commit()
pool.get('ir.actions.report.xml').register_all(cr) pool.get('ir.actions.report.xml').register_all(cr)
openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
return True return True
def report_get(self, cr, uid, report_id, context=None): def report_get(self, cr, uid, report_id, context=None):

View File

@ -92,9 +92,9 @@ class ExportToRML( unohelper.Base, XJobExecutor ):
if docinfo.getUserFieldValue(2) == "": if docinfo.getUserFieldValue(2) == "":
ErrorDialog("Please Save this file on server","Use Send To Server Option in OpenERP Report Menu","Error") ErrorDialog("Please Save this file on server","Use Send To Server Option in OpenERP Report Menu","Error")
exit(1) exit(1)
filename = self.GetAFileName() filename = self.GetAFileName()
if not filename: if not filename:
exit(1) exit(1)
global passwd global passwd
self.password = passwd self.password = passwd
try: try:
@ -118,7 +118,7 @@ class ExportToRML( unohelper.Base, XJobExecutor ):
initPath = tempfile.gettempdir() initPath = tempfile.gettempdir()
oUcb = createUnoService("com.sun.star.ucb.SimpleFileAccess") oUcb = createUnoService("com.sun.star.ucb.SimpleFileAccess")
if oUcb.exists(initPath): if oUcb.exists(initPath):
oFileDialog.setDisplayDirectory('file://' + ( os.name == 'nt' and '/' or '' ) + initPath ) oFileDialog.setDisplayDirectory('file://' + ( os.name == 'nt' and '/' or '' ) + initPath )
oFileDialog.setDefaultName(f_path ) oFileDialog.setDefaultName(f_path )

View File

@ -209,13 +209,13 @@ class Fields(unohelper.Base, XJobExecutor ):
key.sort() key.sort()
myval=None myval=None
if not sVar.find("/")==-1: if not sVar.find("/")==-1:
myval=sVar[:sVar.find("/")] myval=sVar[:sVar.find("/")]
else: else:
myval=sVar myval=sVar
if myval in key: if myval in key:
if (res[myval]['type'] in ['many2one']): if (res[myval]['type'] in ['many2one']):
sObject = res[myval]['relation'] sObject = res[myval]['relation']
return self.getRes(sock,res[myval]['relation'], sVar[sVar.find("/")+1:]) return self.getRes(sock,res[myval]['relation'], sVar[sVar.find("/")+1:])
else: else:
return sObject return sObject

View File

@ -166,33 +166,33 @@ class RepeatIn( unohelper.Base, XJobExecutor ):
self.sValue= "objects" self.sValue= "objects"
else: else:
sItem="" sItem=""
for anObject in self.aObjectList: for anObject in self.aObjectList:
if anObject[:anObject.find("(")] == sObject: if anObject[:anObject.find("(")] == sObject:
sItem = anObject sItem = anObject
self.insVariable.setText( sItem ) self.insVariable.setText( sItem )
genTree( genTree(
sItem[sItem.find("(")+1:sItem.find(")")], sItem[sItem.find("(")+1:sItem.find(")")],
self.aListRepeatIn, self.aListRepeatIn,
self.insField, self.insField,
self.sMyHost, self.sMyHost,
2, 2,
ending=['one2many','many2many'], ending=['one2many','many2many'],
recur=['one2many','many2many'] recur=['one2many','many2many']
) )
self.sValue= self.win.getListBoxItem("lstFields",self.aListRepeatIn.index(sFields)) self.sValue= self.win.getListBoxItem("lstFields",self.aListRepeatIn.index(sFields))
for var in self.aVariableList: for var in self.aVariableList:
if var[:8] <> 'List of ': if var[:8] <> 'List of ':
self.model_ids = self.sock.execute(database, uid, self.password, 'ir.model' , 'search', [('model','=',var[var.find("(")+1:var.find(")")])]) self.model_ids = self.sock.execute(database, uid, self.password, 'ir.model' , 'search', [('model','=',var[var.find("(")+1:var.find(")")])])
else: else:
self.model_ids = self.sock.execute(database, uid, self.password, 'ir.model' , 'search', [('model','=',var[8:])]) self.model_ids = self.sock.execute(database, uid, self.password, 'ir.model' , 'search', [('model','=',var[8:])])
fields=['name','model'] fields=['name','model']
self.model_res = self.sock.execute(database, uid, self.password, 'ir.model', 'read', self.model_ids,fields) self.model_res = self.sock.execute(database, uid, self.password, 'ir.model', 'read', self.model_ids,fields)
if self.model_res <> []: if self.model_res <> []:
if var[:8]<>'List of ': if var[:8]<>'List of ':
self.insVariable.addItem(var[:var.find("(")+1] + self.model_res[0]['name'] + ")" ,self.insVariable.getItemCount()) self.insVariable.addItem(var[:var.find("(")+1] + self.model_res[0]['name'] + ")" ,self.insVariable.getItemCount())
else: else:
self.insVariable.addItem('List of ' + self.model_res[0]['name'] ,self.insVariable.getItemCount()) self.insVariable.addItem('List of ' + self.model_res[0]['name'] ,self.insVariable.getItemCount())
@ -212,8 +212,8 @@ class RepeatIn( unohelper.Base, XJobExecutor ):
self.win.setEditText("txtName", self.sGVariable) self.win.setEditText("txtName", self.sGVariable)
self.win.setEditText("txtUName",self.sGDisplayName) self.win.setEditText("txtUName",self.sGDisplayName)
else: else:
self.win.setEditText("txtName",sMain[sMain.rfind("/")+1:]) self.win.setEditText("txtName",sMain[sMain.rfind("/")+1:])
self.win.setEditText("txtUName","|-."+sItem[sItem.rfind("/")+1:]+".-|") self.win.setEditText("txtUName","|-."+sItem[sItem.rfind("/")+1:]+".-|")
def cmbVariable_selected(self, oItemEvent): def cmbVariable_selected(self, oItemEvent):
@ -225,15 +225,15 @@ class RepeatIn( unohelper.Base, XJobExecutor ):
self.win.removeListBoxItems("lstFields", 0, self.win.getListBoxItemCount("lstFields")) self.win.removeListBoxItems("lstFields", 0, self.win.getListBoxItemCount("lstFields"))
sItem=self.win.getComboBoxText("cmbVariable") sItem=self.win.getComboBoxText("cmbVariable")
for var in self.aVariableList: for var in self.aVariableList:
if var[:8]=='List of ': if var[:8]=='List of ':
if var[:8]==sItem[:8]: if var[:8]==sItem[:8]:
sItem = var sItem = var
elif var[:var.find("(")+1] == sItem[:sItem.find("(")+1]: elif var[:var.find("(")+1] == sItem[:sItem.find("(")+1]:
sItem = var sItem = var
self.aListRepeatIn=[] self.aListRepeatIn=[]
data = ( sItem[sItem.rfind(" ") + 1:] == docinfo.getUserFieldValue(3) ) and docinfo.getUserFieldValue(3) or sItem[sItem.find("(")+1:sItem.find(")")] data = ( sItem[sItem.rfind(" ") + 1:] == docinfo.getUserFieldValue(3) ) and docinfo.getUserFieldValue(3) or sItem[sItem.find("(")+1:sItem.find(")")]
genTree( data, self.aListRepeatIn, self.insField, self.sMyHost, 2, ending=['one2many','many2many'], recur=['one2many','many2many'] ) genTree( data, self.aListRepeatIn, self.insField, self.sMyHost, 2, ending=['one2many','many2many'], recur=['one2many','many2many'] )
self.win.selectListBoxItemPos("lstFields", 0, True ) self.win.selectListBoxItemPos("lstFields", 0, True )

View File

@ -123,6 +123,7 @@ class SendtoServer(unohelper.Base, XJobExecutor):
self.win.addFixedText("lblReportName", 2, 30, 50, 15, "Technical Name :") self.win.addFixedText("lblReportName", 2, 30, 50, 15, "Technical Name :")
self.win.addEdit("txtReportName", -5, 25, 123, 15,report_name) self.win.addEdit("txtReportName", -5, 25, 123, 15,report_name)
self.win.addCheckBox("chkHeader", 51, 45, 70 ,15, "Corporate Header") self.win.addCheckBox("chkHeader", 51, 45, 70 ,15, "Corporate Header")
self.win.setCheckBoxState("chkHeader", True)
self.win.addFixedText("lblResourceType", 2 , 60, 50, 15, "Select Rpt. Type :") self.win.addFixedText("lblResourceType", 2 , 60, 50, 15, "Select Rpt. Type :")
self.win.addComboListBox("lstResourceType", -5, 58, 123, 15,True,itemListenerProc=self.lstbox_selected) self.win.addComboListBox("lstResourceType", -5, 58, 123, 15,True,itemListenerProc=self.lstbox_selected)
self.lstResourceType = self.win.getControl( "lstResourceType" ) self.lstResourceType = self.win.getControl( "lstResourceType" )
@ -190,7 +191,6 @@ class SendtoServer(unohelper.Base, XJobExecutor):
#sock = xmlrpclib.ServerProxy(docinfo.getUserFieldValue(0) +'/xmlrpc/object') #sock = xmlrpclib.ServerProxy(docinfo.getUserFieldValue(0) +'/xmlrpc/object')
file_type = oDoc2.getURL()[7:].split(".")[-1] file_type = oDoc2.getURL()[7:].split(".")[-1]
res = self.sock.execute(database, uid, self.password, 'ir.actions.report.xml', 'upload_report', int(docinfo.getUserFieldValue(2)),base64.encodestring(data),file_type,{})
params = { params = {
'name': self.win.getEditText("txtName"), 'name': self.win.getEditText("txtName"),
'model': docinfo.getUserFieldValue(3), 'model': docinfo.getUserFieldValue(3),
@ -200,7 +200,12 @@ class SendtoServer(unohelper.Base, XJobExecutor):
} }
if self.win.getListBoxSelectedItem("lstResourceType")=='OpenOffice': if self.win.getListBoxSelectedItem("lstResourceType")=='OpenOffice':
params['report_type']=file_type params['report_type']=file_type
res = self.sock.execute(database, uid, self.password, 'ir.actions.report.xml', 'write', int(docinfo.getUserFieldValue(2)), params) self.sock.execute(database, uid, self.password, 'ir.actions.report.xml', 'write', int(docinfo.getUserFieldValue(2)), params)
# Call upload_report as the *last* step, as it will call register_all() and cause the report service
# to be loaded - which requires all the data to be correct in the database
self.sock.execute(database, uid, self.password, 'ir.actions.report.xml', 'upload_report', int(docinfo.getUserFieldValue(2)),base64.encodestring(data),file_type,{})
self.logobj.log_write('SendToServer',LOG_INFO, ':Report %s successfully send using %s'%(params['name'],database)) self.logobj.log_write('SendToServer',LOG_INFO, ':Report %s successfully send using %s'%(params['name'],database))
self.win.endExecute() self.win.endExecute()
else: else:

View File

@ -112,43 +112,43 @@ class AddLang(unohelper.Base, XJobExecutor ):
text=cursor.getText() text=cursor.getText()
tcur=text.createTextCursorByRange(cursor) tcur=text.createTextCursorByRange(cursor)
self.aVariableList.extend( filter( lambda obj: obj[:obj.find("(")] == "Objects", self.aObjectList ) ) self.aVariableList.extend( filter( lambda obj: obj[:obj.find("(")] == "Objects", self.aObjectList ) )
for i in range(len(self.aItemList)): for i in range(len(self.aItemList)):
anItem = self.aItemList[i][1] anItem = self.aItemList[i][1]
component = self.aComponentAdd[i] component = self.aComponentAdd[i]
if component == "Document": if component == "Document":
sLVal = anItem[anItem.find(",'") + 2:anItem.find("')")] sLVal = anItem[anItem.find(",'") + 2:anItem.find("')")]
self.aVariableList.extend( filter( lambda obj: obj[:obj.find("(")] == sLVal, self.aObjectList ) ) self.aVariableList.extend( filter( lambda obj: obj[:obj.find("(")] == sLVal, self.aObjectList ) )
if tcur.TextSection: if tcur.TextSection:
getRecersiveSection(tcur.TextSection,self.aSectionList) getRecersiveSection(tcur.TextSection,self.aSectionList)
if component in self.aSectionList: if component in self.aSectionList:
sLVal = anItem[anItem.find(",'") + 2:anItem.find("')")] sLVal = anItem[anItem.find(",'") + 2:anItem.find("')")]
self.aVariableList.extend( filter( lambda obj: obj[:obj.find("(")] == sLVal, self.aObjectList ) ) self.aVariableList.extend( filter( lambda obj: obj[:obj.find("(")] == sLVal, self.aObjectList ) )
if tcur.TextTable: if tcur.TextTable:
if not component == "Document" and component[component.rfind(".") + 1:] == tcur.TextTable.Name: if not component == "Document" and component[component.rfind(".") + 1:] == tcur.TextTable.Name:
VariableScope(tcur,self.insVariable,self.aObjectList,self.aComponentAdd,self.aItemList,component) VariableScope(tcur,self.insVariable,self.aObjectList,self.aComponentAdd,self.aItemList,component)
self.bModify=bFromModify self.bModify=bFromModify
if self.bModify==True: if self.bModify==True:
sItem="" sItem=""
for anObject in self.aObjectList: for anObject in self.aObjectList:
if anObject[:anObject.find("(")] == sVariable: if anObject[:anObject.find("(")] == sVariable:
sItem = anObject sItem = anObject
self.insVariable.setText( sItem ) self.insVariable.setText( sItem )
genTree(sItem[sItem.find("(")+1:sItem.find(")")],self.aListFields, self.insField,self.sMyHost,2,ending_excl=['one2many','many2one','many2many','reference'], recur=['many2one']) genTree(sItem[sItem.find("(")+1:sItem.find(")")],self.aListFields, self.insField,self.sMyHost,2,ending_excl=['one2many','many2one','many2many','reference'], recur=['many2one'])
self.sValue= self.win.getListBoxItem("lstFields",self.aListFields.index(sFields)) self.sValue= self.win.getListBoxItem("lstFields",self.aListFields.index(sFields))
for var in self.aVariableList: for var in self.aVariableList:
self.model_ids = self.sock.execute(database, uid, self.password, 'ir.model' , 'search', [('model','=',var[var.find("(")+1:var.find(")")])]) self.model_ids = self.sock.execute(database, uid, self.password, 'ir.model' , 'search', [('model','=',var[var.find("(")+1:var.find(")")])])
fields=['name','model'] fields=['name','model']
self.model_res = self.sock.execute(database, uid, self.password, 'ir.model', 'read', self.model_ids,fields) self.model_res = self.sock.execute(database, uid, self.password, 'ir.model', 'read', self.model_ids,fields)
if self.model_res <> []: if self.model_res <> []:
self.insVariable.addItem(var[:var.find("(")+1] + self.model_res[0]['name'] + ")" ,self.insVariable.getItemCount()) self.insVariable.addItem(var[:var.find("(")+1] + self.model_res[0]['name'] + ")" ,self.insVariable.getItemCount())
else: else:
self.insVariable.addItem(var ,self.insVariable.getItemCount()) self.insVariable.addItem(var ,self.insVariable.getItemCount())
@ -165,15 +165,15 @@ class AddLang(unohelper.Base, XJobExecutor ):
docinfo=doc.getDocumentInfo() docinfo=doc.getDocumentInfo()
sItem= self.win.getComboBoxText("cmbVariable") sItem= self.win.getComboBoxText("cmbVariable")
for var in self.aVariableList: for var in self.aVariableList:
if var[:var.find("(")+1]==sItem[:sItem.find("(")+1]: if var[:var.find("(")+1]==sItem[:sItem.find("(")+1]:
sItem = var sItem = var
sMain=self.aListFields[self.win.getListBoxSelectedItemPos("lstFields")] sMain=self.aListFields[self.win.getListBoxSelectedItemPos("lstFields")]
t=sMain.rfind('/lang') t=sMain.rfind('/lang')
if t!=-1: if t!=-1:
sObject=self.getRes(self.sock,sItem[sItem.find("(")+1:-1],sMain[1:]) sObject=self.getRes(self.sock,sItem[sItem.find("(")+1:-1],sMain[1:])
ids = self.sock.execute(database, uid, self.password, sObject , 'search', []) ids = self.sock.execute(database, uid, self.password, sObject , 'search', [])
res = self.sock.execute(database, uid, self.password, sObject , 'read',[ids[0]]) res = self.sock.execute(database, uid, self.password, sObject , 'read',[ids[0]])
self.win.setEditText("txtUName",res[0][sMain[sMain.rfind("/")+1:]]) self.win.setEditText("txtUName",res[0][sMain[sMain.rfind("/")+1:]])
else: else:
ErrorDialog("Please select a language.") ErrorDialog("Please select a language.")
@ -192,13 +192,13 @@ class AddLang(unohelper.Base, XJobExecutor ):
key.sort() key.sort()
myval=None myval=None
if not sVar.find("/")==-1: if not sVar.find("/")==-1:
myval=sVar[:sVar.find("/")] myval=sVar[:sVar.find("/")]
else: else:
myval=sVar myval=sVar
if myval in key: if myval in key:
if (res[myval]['type'] in ['many2one']): if (res[myval]['type'] in ['many2one']):
sObject = res[myval]['relation'] sObject = res[myval]['relation']
return self.getRes(sock,res[myval]['relation'], sVar[sVar.find("/")+1:]) return self.getRes(sock,res[myval]['relation'], sVar[sVar.find("/")+1:])
else: else:
return sObject return sObject
@ -213,18 +213,18 @@ class AddLang(unohelper.Base, XJobExecutor ):
self.aListFields=[] self.aListFields=[]
tempItem = self.win.getComboBoxText("cmbVariable") tempItem = self.win.getComboBoxText("cmbVariable")
for var in self.aVariableList: for var in self.aVariableList:
if var[:var.find("(")] == tempItem[:tempItem.find("(")]: if var[:var.find("(")] == tempItem[:tempItem.find("(")]:
sItem=var sItem=var
genTree( genTree(
sItem[ sItem.find("(") + 1:sItem.find(")")], sItem[ sItem.find("(") + 1:sItem.find(")")],
self.aListFields, self.aListFields,
self.insField, self.insField,
self.sMyHost, self.sMyHost,
2, 2,
ending_excl=['one2many','many2one','many2many','reference'], ending_excl=['one2many','many2one','many2many','reference'],
recur=['many2one'] recur=['many2one']
) )
except: except:
import traceback;traceback.print_exc() import traceback;traceback.print_exc()

View File

@ -87,19 +87,19 @@ def genTree(object, aList, insField, host, level=3, ending=None, ending_excl=Non
def VariableScope(oTcur, insVariable, aObjectList, aComponentAdd, aItemList, sTableName=""): def VariableScope(oTcur, insVariable, aObjectList, aComponentAdd, aItemList, sTableName=""):
if sTableName.find(".") != -1: if sTableName.find(".") != -1:
for i in range(len(aItemList)): for i in range(len(aItemList)):
if aComponentAdd[i]==sTableName: if aComponentAdd[i]==sTableName:
sLVal=aItemList[i][1][aItemList[i][1].find(",'")+2:aItemList[i][1].find("')")] sLVal=aItemList[i][1][aItemList[i][1].find(",'")+2:aItemList[i][1].find("')")]
for j in range(len(aObjectList)): for j in range(len(aObjectList)):
if aObjectList[j][:aObjectList[j].find("(")] == sLVal: if aObjectList[j][:aObjectList[j].find("(")] == sLVal:
insVariable.append(aObjectList[j]) insVariable.append(aObjectList[j])
VariableScope(oTcur,insVariable,aObjectList,aComponentAdd,aItemList, sTableName[:sTableName.rfind(".")]) VariableScope(oTcur,insVariable,aObjectList,aComponentAdd,aItemList, sTableName[:sTableName.rfind(".")])
else: else:
for i in range(len(aItemList)): for i in range(len(aItemList)):
if aComponentAdd[i]==sTableName: if aComponentAdd[i]==sTableName:
sLVal=aItemList[i][1][aItemList[i][1].find(",'")+2:aItemList[i][1].find("')")] sLVal=aItemList[i][1][aItemList[i][1].find(",'")+2:aItemList[i][1].find("')")]
for j in range(len(aObjectList)): for j in range(len(aObjectList)):
if aObjectList[j][:aObjectList[j].find("(")] == sLVal and sLVal!="": if aObjectList[j][:aObjectList[j].find("(")] == sLVal and sLVal!="":
insVariable.append(aObjectList[j]) insVariable.append(aObjectList[j])
def getList(aObjectList, host, count): def getList(aObjectList, host, count):
@ -145,8 +145,8 @@ def getRelation(sRelName, sItem, sObjName, aObjectList, host):
if k == sItem: if k == sItem:
aObjectList.append(sObjName + "(" + res[k]['relation'] + ")") aObjectList.append(sObjName + "(" + res[k]['relation'] + ")")
return 0 return 0
if k == sItem[:sItem.find(".")]: if k == sItem[:sItem.find(".")]:
getRelation(res[k]['relation'], sItem[sItem.find(".")+1:], sObjName,aObjectList,host) getRelation(res[k]['relation'], sItem[sItem.find(".")+1:], sObjName,aObjectList,host)
def getPath(sPath, sMain): def getPath(sPath, sMain):
@ -157,13 +157,13 @@ def getPath(sPath, sMain):
oPar = oParEnum.nextElement() oPar = oParEnum.nextElement()
if oPar.supportsService("com.sun.star.text.TextField.DropDown"): if oPar.supportsService("com.sun.star.text.TextField.DropDown"):
sItem=oPar.Items[1] sItem=oPar.Items[1]
if sPath[:sPath.find(".")] == sMain: if sPath[:sPath.find(".")] == sMain:
break; break;
else: else:
res = re.findall('\\[\\[ *([a-zA-Z0-9_\.]+) *\\]\\]',sPath) res = re.findall('\\[\\[ *([a-zA-Z0-9_\.]+) *\\]\\]',sPath)
if len(res) <> 0: if len(res) <> 0:
if sItem[sItem.find(",'")+2:sItem.find("')")] == sPath[:sPath.find(".")]: if sItem[sItem.find(",'")+2:sItem.find("')")] == sPath[:sPath.find(".")]:
sPath = sItem[sItem.find("(")+1:sItem.find(",")] + sPath[sPath.find("."):] sPath = sItem[sItem.find("(")+1:sItem.find(",")] + sPath[sPath.find("."):]
getPath(sPath, sMain) getPath(sPath, sMain)
return sPath return sPath

View File

@ -22,21 +22,21 @@
import urllib import urllib
def get_absolute_file_path(url): def get_absolute_file_path(url):
url_unquoted = urllib.unquote(url) url_unquoted = urllib.unquote(url)
return os.name == 'nt' and url_unquoted[1:] or url_unquoted return os.name == 'nt' and url_unquoted[1:] or url_unquoted
# This function reads the content of a file and return it to the caller # This function reads the content of a file and return it to the caller
def read_data_from_file(filename): def read_data_from_file(filename):
fp = file( filename, "rb" ) fp = file( filename, "rb" )
data = fp.read() data = fp.read()
fp.close() fp.close()
return data return data
# This function writes the content to a file # This function writes the content to a file
def write_data_to_file(filename, data): def write_data_to_file(filename, data):
fp = file( filename, 'wb' ) fp = file( filename, 'wb' )
fp.write( data ) fp.write( data )
fp.close() fp.close()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -75,42 +75,42 @@ class modify(unohelper.Base, XJobExecutor ):
self.sMyHost= docinfo.getUserFieldValue(0) self.sMyHost= docinfo.getUserFieldValue(0)
else: else:
ErrorDialog( ErrorDialog(
"Please insert user define field Field-1", "Please insert user define field Field-1",
"Just go to File->Properties->User Define \n" "Just go to File->Properties->User Define \n"
"Field-1 E.g. http://localhost:8069" "Field-1 E.g. http://localhost:8069"
) )
exit(1) exit(1)
# Check weather Field-4 is available or not otherwise exit from application # Check weather Field-4 is available or not otherwise exit from application
if not docinfo.getUserFieldValue(3) == "" and not docinfo.getUserFieldValue(0)=="": if not docinfo.getUserFieldValue(3) == "" and not docinfo.getUserFieldValue(0)=="":
if self.oVC.TextField: if self.oVC.TextField:
self.oCurObj=self.oVC.TextField self.oCurObj=self.oVC.TextField
item = self.oCurObj.Items[0] item = self.oCurObj.Items[0]
kind, group1, group2 = self.getOperation(self.oCurObj.Items[1] ) kind, group1, group2 = self.getOperation(self.oCurObj.Items[1] )
start_group1 = group1[:group1.find(".")] start_group1 = group1[:group1.find(".")]
stop_group1 = group1[group1.find("."):].replace(".", "/") stop_group1 = group1[group1.find("."):].replace(".", "/")
if kind == "field": if kind == "field":
Fields( start_group1, stop_group1, item, True ) Fields( start_group1, stop_group1, item, True )
elif kind == "expression": elif kind == "expression":
Expression( group1, item, True ) Expression( group1, item, True )
elif kind == "repeatIn": elif kind == "repeatIn":
RepeatIn( start_group1, group2, stop_group1, item, True ) RepeatIn( start_group1, group2, stop_group1, item, True )
else: else:
ErrorDialog( ErrorDialog(
"Please place your cursor at beginning of field that you want to modify.","" "Please place your cursor at beginning of field that you want to modify.",""
) )
else: else:
ErrorDialog( ErrorDialog(
"Please insert user define field Field-1 or Field-4", "Please insert user define field Field-1 or Field-4",
"Just go to File->Properties->User Define \n" "Just go to File->Properties->User Define \n"
"Field-1 E.g. http://localhost:8069 \n" "Field-1 E.g. http://localhost:8069 \n"
"OR \n" "OR \n"
"Field-4 E.g. account.invoice" "Field-4 E.g. account.invoice"
) )
exit(1) exit(1)
def getOperation(self, str): def getOperation(self, str):
@ -121,14 +121,14 @@ class modify(unohelper.Base, XJobExecutor ):
method2 = lambda x: (u'field', x.group(1), None) method2 = lambda x: (u'field', x.group(1), None)
method3 = lambda x: (u'expression', x.group(1), None) method3 = lambda x: (u'expression', x.group(1), None)
regexes = [ regexes = [
('\\[\\[ *repeatIn\\( *(.+)*, *\'([a-zA-Z0-9_]+)\' *\\) *\\]\\]', method1), ('\\[\\[ *repeatIn\\( *(.+)*, *\'([a-zA-Z0-9_]+)\' *\\) *\\]\\]', method1),
('\\[\\[ *([a-zA-Z0-9_\.]+) *\\]\\]', method2), ('\\[\\[ *([a-zA-Z0-9_\.]+) *\\]\\]', method2),
('\\[\\[ *(.+) *\\]\\]', method3) ('\\[\\[ *(.+) *\\]\\]', method3)
] ]
for (rule,method) in regexes: for (rule,method) in regexes:
res = re.match(rule, str) res = re.match(rule, str)
if res: if res:
return method(res) return method(res)
if __name__<>"package": if __name__<>"package":
modify(None) modify(None)

View File

@ -11,23 +11,23 @@ import time
sock = xmlrpclib.ServerProxy('http://localhost:8069/xmlrpc/object') sock = xmlrpclib.ServerProxy('http://localhost:8069/xmlrpc/object')
def get(object, level=3, ending=None, ending_excl=None, recur=None, root=''): def get(object, level=3, ending=None, ending_excl=None, recur=None, root=''):
if ending is None: if ending is None:
ending = [] ending = []
if ending_excl is None: if ending_excl is None:
ending_excl = [] ending_excl = []
if recur is None: if recur is None:
recur = [] recur = []
res = sock.execute('terp', 3, 'admin', 'account.invoice', 'fields_get') res = sock.execute('terp', 3, 'admin', 'account.invoice', 'fields_get')
key = res.keys() key = res.keys()
key.sort() key.sort()
for k in key: for k in key:
if (not ending or res[k]['type'] in ending) and ((not ending_excl) or not (res[k]['type'] in ending_excl)): if (not ending or res[k]['type'] in ending) and ((not ending_excl) or not (res[k]['type'] in ending_excl)):
print root+'/'+k print root+'/'+k
if res[k]['type'] in recur: if res[k]['type'] in recur:
print root+'/'+k print root+'/'+k
if (res[k]['type'] in recur) and (level>0): if (res[k]['type'] in recur) and (level>0):
get(res[k]['relation'], level-1, ending, ending_excl, recur, root+'/'+k) get(res[k]['relation'], level-1, ending, ending_excl, recur, root+'/'+k)
print 'Field selection for a rields', '='*40 print 'Field selection for a rields', '='*40
get('account.invoice', level=0, ending_excl=['one2many','many2one','many2many','reference'], recur=['many2one']) get('account.invoice', level=0, ending_excl=['one2many','many2one','many2many','reference'], recur=['many2one'])

View File

@ -177,8 +177,10 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({
view_mode = action_attrs.view_mode; view_mode = action_attrs.view_mode;
// evaluate action_attrs context and domain // evaluate action_attrs context and domain
action_attrs.context_string = action_attrs.context;
action_attrs.context = instance.web.pyeval.eval( action_attrs.context = instance.web.pyeval.eval(
'context', action_attrs.context || {}); 'context', action_attrs.context || {});
action_attrs.domain_string = action_attrs.domain;
action_attrs.domain = instance.web.pyeval.eval( action_attrs.domain = instance.web.pyeval.eval(
'domain', action_attrs.domain || [], action_attrs.context); 'domain', action_attrs.domain || [], action_attrs.context);
if (action_attrs.context['dashboard_merge_domains_contexts'] === false) { if (action_attrs.context['dashboard_merge_domains_contexts'] === false) {
@ -301,9 +303,9 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({
instance.web.form.DashBoardLegacy = instance.web.form.DashBoard.extend({ instance.web.form.DashBoardLegacy = instance.web.form.DashBoard.extend({
renderElement: function() { renderElement: function() {
if (this.node.tag == 'hpaned') { if (this.node.tag == 'hpaned') {
this.node.attrs.style = '2-1'; this.node.attrs.layout = '2-1';
} else if (this.node.tag == 'vpaned') { } else if (this.node.tag == 'vpaned') {
this.node.attrs.style = '1'; this.node.attrs.layout = '1';
} }
this.node.tag = 'board'; this.node.tag = 'board';
_.each(this.node.children, function(child) { _.each(this.node.children, function(child) {

View File

@ -10,7 +10,7 @@
<span> Change Layout </span> <span> Change Layout </span>
</button> </button>
</div> </div>
<table t-att-data-layout="node.attrs.style" t-attf-class="oe_dashboard oe_dashboard_layout_#{node.attrs.style}" cellspacing="0" cellpadding="0" border="0"> <table t-att-data-layout="node.attrs.layout" t-attf-class="oe_dashboard oe_dashboard_layout_#{node.attrs.layout}" cellspacing="0" cellpadding="0" border="0">
<tr> <tr>
<td t-foreach="node.children" t-as="column" t-if="column.tag == 'column'" <td t-foreach="node.children" t-as="column" t-if="column.tag == 'column'"
t-att-id="view.element_id + '_column_' + column_index" t-attf-class="oe_dashboard_column index_#{column_index}"> t-att-id="view.element_id + '_column_' + column_index" t-attf-class="oe_dashboard_column index_#{column_index}">

View File

@ -80,8 +80,7 @@
basis for email recipients, name and to ease the definition of a further elaborated template. --> basis for email recipients, name and to ease the definition of a further elaborated template. -->
<record id="email_template_opportunity_mail" model="email.template"> <record id="email_template_opportunity_mail" model="email.template">
<field name="name">Opportunity - Send Emails</field> <field name="name">Opportunity - Send Emails</field>
<field name="email_from">${object.user_id.email or ''}</field> <field name="subject">${object.name}</field>
<field name="subject">Opportunity ${object.name | h})</field>
<field name="model_id" ref="crm.model_crm_lead"/> <field name="model_id" ref="crm.model_crm_lead"/>
<field name="auto_delete" eval="True"/> <field name="auto_delete" eval="True"/>
<field name="email_recipients">${object.partner_id.id}</field> <field name="email_recipients">${object.partner_id.id}</field>

View File

@ -259,7 +259,9 @@ class crm_lead(base_stage, format_address, osv.osv):
'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"), 'channel_id': fields.many2one('crm.case.channel', 'Channel', help="Communication channel (mail, direct, phone, ...)"),
'contact_name': fields.char('Contact Name', size=64), 'contact_name': fields.char('Contact Name', size=64),
'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1), 'partner_name': fields.char("Customer Name", size=64,help='The name of the future partner company that will be created while converting the lead into opportunity', select=1),
'opt_out': fields.boolean('Opt-Out', oldname='optout', help="If opt-out is checked, this contact has refused to receive emails or unsubscribed to a campaign."), 'opt_out': fields.boolean('Opt-Out', oldname='optout',
help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
"Filter 'Available for Mass Mailing' allows users to filter the leads when performing mass mailing."),
'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"), 'type':fields.selection([ ('lead','Lead'), ('opportunity','Opportunity'), ],'Type', help="Type is used to separate Leads and Opportunities"),
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True), 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
'date_closed': fields.datetime('Closed', readonly=True), 'date_closed': fields.datetime('Closed', readonly=True),
@ -711,7 +713,7 @@ class crm_lead(base_stage, format_address, osv.osv):
'parent_id': parent_id, 'parent_id': parent_id,
'phone': lead.phone, 'phone': lead.phone,
'mobile': lead.mobile, 'mobile': lead.mobile,
'email': lead.email_from and tools.email_split(lead.email_from)[0], 'email': tools.email_split(lead.email_from) and tools.email_split(lead.email_from)[0] or False,
'fax': lead.fax, 'fax': lead.fax,
'title': lead.title and lead.title.id or False, 'title': lead.title and lead.title.id or False,
'function': lead.function, 'function': lead.function,
@ -930,7 +932,7 @@ class crm_lead(base_stage, format_address, osv.osv):
try: try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1] compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError: except ValueError:
compose_form_id = False compose_form_id = False
if context is None: if context is None:
context = {} context = {}
ctx = context.copy() ctx = context.copy()
@ -961,13 +963,22 @@ class crm_lead(base_stage, format_address, osv.osv):
return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
for lead in self.browse(cr, uid, ids, context=context)] for lead in self.browse(cr, uid, ids, context=context)]
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for lead in self.browse(cr, uid, ids, context=context):
if lead.partner_id:
self._message_add_suggested_recipient(cr, uid, recipients, lead, partner=lead.partner_id, reason=_('Customer'))
elif lead.email_from:
self._message_add_suggested_recipient(cr, uid, recipients, lead, email=lead.email_from, reason=_('Customer Email'))
return recipients
def message_new(self, cr, uid, msg, custom_values=None, context=None): def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway """ Overrides mail_thread message_new that is called by the mailgateway
through message_process. through message_process.
This override updates the document according to the email. This override updates the document according to the email.
""" """
if custom_values is None: custom_values = {} if custom_values is None:
custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else '' desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
defaults = { defaults = {
'name': msg.get('subject') or _("No Subject"), 'name': msg.get('subject') or _("No Subject"),
@ -1015,9 +1026,12 @@ class crm_lead(base_stage, format_address, osv.osv):
def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None): def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0] phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
if action == 'log': prefix = 'Logged' if action == 'log':
else: prefix = 'Scheduled' prefix = 'Logged'
message = _("<b>%s a call</b> for the <em>%s</em>.") % (prefix, phonecall.date) else:
prefix = 'Scheduled'
suffix = ' %s' % phonecall.description
message = _("%s a call for %s.%s") % (prefix, phonecall.date, suffix)
return self.message_post(cr, uid, ids, body=message, context=context) return self.message_post(cr, uid, ids, body=message, context=context)
def onchange_state(self, cr, uid, ids, state_id, context=None): def onchange_state(self, cr, uid, ids, state_id, context=None):

View File

@ -97,7 +97,7 @@
states="draft,open,pending" help="Convert to Opportunity" class="oe_highlight"/> states="draft,open,pending" help="Convert to Opportunity" class="oe_highlight"/>
<button name="case_reset" string="Reset" type="object" <button name="case_reset" string="Reset" type="object"
states="cancel"/> states="cancel"/>
<button name="case_cancel" string="Cancel" type="object" <button name="case_cancel" string="Cancel Case" type="object"
states="draft,open,pending"/> states="draft,open,pending"/>
<field name="stage_id" widget="statusbar" clickable="True" <field name="stage_id" widget="statusbar" clickable="True"
on_change="onchange_stage_id(stage_id)"/> on_change="onchange_stage_id(stage_id)"/>
@ -163,7 +163,9 @@
<field name="priority"/> <field name="priority"/>
<field name="categ_ids" <field name="categ_ids"
widget="many2many_tags" widget="many2many_tags"
domain="[('object_id.model','=','crm.lead')]"/> domain="[('object_id.model','=','crm.lead')]"
context="{'object_name': 'crm.lead'}"
/>
</group> </group>
</group> </group>
<notebook colspan="4"> <notebook colspan="4">
@ -339,18 +341,21 @@
<filter string="Assigned to My Team(s)" <filter string="Assigned to My Team(s)"
domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}" domain="[('section_id.member_ids', 'in', [uid])]" context="{'invisible_section': False}"
help="Leads that are assigned to any sales teams I am member of"/> help="Leads that are assigned to any sales teams I am member of"/>
<separator/> <separator />
<filter string="Available for mass mailing"
name='not_opt_out' domain="[('opt_out', '=', False)]"
help="Leads that did not ask not to be included in mass mailing campaigns"/>
<separator />
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/> <filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Team" domain="[]" context="{'group_by':'section_id'}"/> <filter string="Team" domain="[]" context="{'group_by':'section_id'}"/>
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/> <filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/> <filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Country" domain="[]" context="{'group_by':'country_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="Referrer" domain="[]" context="{'group_by':'referred'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/> <filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/> <filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter string="Creation" domain="[]" context="{'group_by':'create_date'}" groups="base.group_no_one"/> <filter string="Creation" domain="[]" context="{'group_by':'create_date'}"/>
</group> </group>
<group string="Display"> <group string="Display">
<filter string="Show Countries" context="{'invisible_country': False}" help="Show Countries"/> <filter string="Show Countries" context="{'invisible_country': False}" help="Show Countries"/>
@ -377,7 +382,6 @@
states="draft,open,pending" class="oe_highlight"/> states="draft,open,pending" class="oe_highlight"/>
<button name="case_mark_lost" string="Mark Lost" type="object" <button name="case_mark_lost" string="Mark Lost" type="object"
states="draft,open" class="oe_highlight"/> states="draft,open" class="oe_highlight"/>
<button name="new_mail_send" string="Send Mail" type="object" states="draft,open,pending"/>
<field name="stage_id" widget="statusbar" clickable="True"/> <field name="stage_id" widget="statusbar" clickable="True"/>
</header> </header>
<sheet> <sheet>
@ -567,7 +571,7 @@
<filter string="Referrer" domain="[]" context="{'group_by':'referred'}"/> <filter string="Referrer" domain="[]" context="{'group_by':'referred'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/> <filter string="Campaign" domain="[]" context="{'group_by':'type_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/> <filter string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter string="Creation" domain="[]" context="{'group_by':'create_date'}" groups="base.group_no_one"/> <filter string="Creation" domain="[]" context="{'group_by':'create_date'}"/>
</group> </group>
<group string="Display"> <group string="Display">
<filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team"/> <filter string="Show Sales Team" context="{'invisible_section': False}" domain="[]" help="Show Sales Team"/>

View File

@ -73,7 +73,7 @@
states="open,pending"/> states="open,pending"/>
<button name="case_reset" string="Reset to Todo" type="object" <button name="case_reset" string="Reset to Todo" type="object"
states="cancel"/> states="cancel"/>
<button name="case_cancel" string="Cancel" type="object" <button name="case_cancel" string="Cancel Call" type="object"
states="draft,open,pending"/> states="draft,open,pending"/>
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="open,done"/> <field name="state" widget="statusbar" nolabel="1" statusbar_visible="open,done"/>
</header> </header>

View File

@ -52,8 +52,6 @@ class crm_opportunity2phonecall(osv.osv_memory):
res.update({'categ_id': categ_id}) res.update({'categ_id': categ_id})
if 'partner_id' in fields: if 'partner_id' in fields:
res.update({'partner_id': opp.partner_id and opp.partner_id.id or False}) res.update({'partner_id': opp.partner_id and opp.partner_id.id or False})
if 'note' in fields:
res.update({'note': opp.description})
if 'contact_name' in fields: if 'contact_name' in fields:
res.update({'contact_name': opp.partner_id and opp.partner_id.name or False}) res.update({'contact_name': opp.partner_id and opp.partner_id.name or False})
if 'phone' in fields: if 'phone' in fields:

View File

@ -13,14 +13,14 @@
<group> <group>
<field name="action"/> <field name="action"/>
<field name="name"/> <field name="name"/>
<field name="categ_id" string="Type"
widget="selection"
domain="[('object_id.model', '=', 'crm.phonecall')]"
groups="base.group_no_one"/>
<field name="date" string="Planned Date" attrs="{'invisible': [('action','=','log')]}"/> <field name="date" string="Planned Date" attrs="{'invisible': [('action','=','log')]}"/>
</group> </group>
<group> <group>
<field name="partner_id" readonly="True"/> <field name="partner_id" readonly="True"/>
<field name="categ_id" string="Type"
widget="selection"
domain="[('object_id.model', '=', 'crm.phonecall')]"
groups="base.group_no_one"/>
<field name="phone"/> <field name="phone"/>
<field name="user_id" attrs="{'invisible': [('action','=','log')]}"/> <field name="user_id" attrs="{'invisible': [('action','=','log')]}"/>
<field name="section_id" widget="selection" attrs="{'invisible': [('action','=','log')]}"/> <field name="section_id" widget="selection" attrs="{'invisible': [('action','=','log')]}"/>

View File

@ -168,7 +168,7 @@
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread" placeholder="Share a note..."/> <field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -31,8 +31,8 @@
<header> <header>
<button name="case_open" string="Open" type="object" class="oe_highlight" <button name="case_open" string="Open" type="object" class="oe_highlight"
states="draft,pending"/> states="draft,pending"/>
<button name="case_close" string="Close" type="object" states="draft,pending"/> <button name="case_close" string="Close Case" type="object" states="draft,pending"/>
<button name="case_close" string="Close" type="object" states="open" class="oe_highlight"/> <button name="case_close" string="Close Case" type="object" states="open" class="oe_highlight"/>
<button name="case_pending" string="Pending" type="object" <button name="case_pending" string="Pending" type="object"
states="draft"/> states="draft"/>
<button name="case_pending" string="Pending" type="object" <button name="case_pending" string="Pending" type="object"
@ -41,7 +41,7 @@
states="cancel,done"/> states="cancel,done"/>
<button name="case_escalate" string="Escalate" type="object" <button name="case_escalate" string="Escalate" type="object"
states="open,draft,pending"/> states="open,draft,pending"/>
<button name="case_cancel" string="Cancel" type="object" <button name="case_cancel" string="Cancel Case" type="object"
states="draft,open,pending"/> states="draft,open,pending"/>
<field name="state" nolabel="1" widget="statusbar" statusbar_visible="draft,open,done" statusbar_colors='{"pending":"blue"}'/> <field name="state" nolabel="1" widget="statusbar" statusbar_visible="draft,open,done" statusbar_colors='{"pending":"blue"}'/>
</header> </header>
@ -95,7 +95,7 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread" placeholder="Share a note..."/> <field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
</div> </div>
</form> </form>

View File

@ -19,7 +19,7 @@
# #
############################################################################## ##############################################################################
import partner_geo_assign import crm_partner_assign
import wizard import wizard
import report import report

View File

@ -44,6 +44,7 @@ You can also use the geolocalization without using the GPS coordinates.
'res_partner_view.xml', 'res_partner_view.xml',
'wizard/crm_forward_to_partner_view.xml', 'wizard/crm_forward_to_partner_view.xml',
'crm_lead_view.xml', 'crm_lead_view.xml',
'crm_partner_assign_data.xml',
'report/crm_lead_report_view.xml', 'report/crm_lead_report_view.xml',
'report/crm_partner_report_view.xml', 'report/crm_partner_report_view.xml',
], ],

View File

@ -71,7 +71,6 @@ class res_partner_grade(osv.osv):
_defaults = { _defaults = {
'active': lambda *args: 1 'active': lambda *args: 1
} }
res_partner_grade()
class res_partner_activation(osv.osv): class res_partner_activation(osv.osv):
_name = 'res.partner.activation' _name = 'res.partner.activation'
@ -82,7 +81,6 @@ class res_partner_activation(osv.osv):
'name' : fields.char('Name', size=32, required=True), 'name' : fields.char('Name', size=32, required=True),
} }
res_partner_activation()
class res_partner(osv.osv): class res_partner(osv.osv):
_inherit = "res.partner" _inherit = "res.partner"
@ -120,7 +118,6 @@ class res_partner(osv.osv):
'date_localization': fields.date.context_today(self,cr,uid,context=context) 'date_localization': fields.date.context_today(self,cr,uid,context=context)
}, context=context) }, context=context)
return True return True
res_partner()
class crm_lead(osv.osv): class crm_lead(osv.osv):
_inherit = "crm.lead" _inherit = "crm.lead"
@ -164,8 +161,9 @@ class crm_lead(osv.osv):
self.assign_geo_localize(cr, uid, [lead.id], lead.partner_latitude, lead.partner_longitude, context=context) self.assign_geo_localize(cr, uid, [lead.id], lead.partner_latitude, lead.partner_longitude, context=context)
partner = res_partner.browse(cr, uid, partner_id, context=context) partner = res_partner.browse(cr, uid, partner_id, context=context)
if partner.user_id: if partner.user_id:
salesteam_id = partner.section_id and partner.section_id.id or False
for lead_id in ids: for lead_id in ids:
self.allocate_salesman(cr, uid, [lead_id], [partner.user_id.id], context=context) self.allocate_salesman(cr, uid, [lead_id], [partner.user_id.id], team_id=salesteam_id, context=context)
self.write(cr, uid, [lead.id], {'date_assign': fields.date.context_today(self,cr,uid,context=context), 'partner_assigned_id': partner_id}, context=context) self.write(cr, uid, [lead.id], {'date_assign': fields.date.context_today(self,cr,uid,context=context), 'partner_assigned_id': partner_id}, context=context)
return res return res
@ -260,7 +258,7 @@ class crm_lead(osv.osv):
res_partner_ids[lead.id] = partner_id res_partner_ids[lead.id] = partner_id
break break
return res_partner_ids return res_partner_ids
crm_lead()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding='UTF-8'?>
<openerp>
<data>
<record id="crm_partner_assign_email_template" model="email.template">
<field name="name">Lead forward</field>
<field name="email_from"></field>
<field name="subject">Fwd: Lead: ${object.name}</field>
<field name="email_to"></field>
<field name="lang"></field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="auto_delete" eval="True"/>
<field name="body_html"><![CDATA[
Hello ${object.partner_assigned_id.name},
<p>
Here is a lead that might interest you.
</p>
<p>
Please keep me informed about your actions about it so that I can keep an
accurate follow-up of it and help you in the sale cycle.
</p>
<p>
Your account manager,<br/>
${object.user_id.name},<br/>
${object.user_id.email}
</p>
<p>
${ctx["mail_body"].replace('\n','<br>') | safe}
</p>
% if ctx["history_mode"] in ('whole'):
% for message in object.message_ids:
---- Original Message (${message.date or ''}) ----<br/>
${message.body | safe}
% endfor
% endif
% if ctx['history_mode'] == 'latest':
---- Original Message (${object.message_ids[0].date or ''}) ----<br/>
${object.message_ids[0].body | safe}
% endif
]]></field>
</record>
</data>
</openerp>

View File

@ -20,30 +20,15 @@
# #
############################################################################## ##############################################################################
import re
import time
from openerp import tools
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.tools.translate import _ from openerp.tools.translate import _
class crm_lead_forward_to_partner(osv.osv_memory):
class crm_lead_forward_to_partner(osv.TransientModel):
""" Forward info history to partners. """ """ Forward info history to partners. """
_name = 'crm.lead.forward.to.partner' _name = 'crm.lead.forward.to.partner'
_inherit = "mail.compose.message" _inherit = "mail.compose.message"
def default_get(self, cr, uid, fields, context=None):
if context is None:
context = {}
# set as comment, perform overrided document-like action that calls get_record_data
old_mode = context.get('default_composition_mode', 'forward')
context['default_composition_mode'] = 'comment'
res = super(crm_lead_forward_to_partner, self).default_get(cr, uid, fields, context=context)
# back to forward mode
context['default_composition_mode'] = old_mode
res['composition_mode'] = context['default_composition_mode']
return res
def _get_composition_mode_selection(self, cr, uid, context=None): def _get_composition_mode_selection(self, cr, uid, context=None):
composition_mode = super(crm_lead_forward_to_partner, self)._get_composition_mode_selection(cr, uid, context=context) composition_mode = super(crm_lead_forward_to_partner, self)._get_composition_mode_selection(cr, uid, context=context)
composition_mode.append(('forward', 'Forward')) composition_mode.append(('forward', 'Forward'))
@ -56,37 +41,54 @@ class crm_lead_forward_to_partner(osv.osv_memory):
'attachment_ids': fields.many2many('ir.attachment', 'attachment_ids': fields.many2many('ir.attachment',
'lead_forward_to_partner_attachment_rel', 'lead_forward_to_partner_attachment_rel',
'wizard_id', 'attachment_id', 'Attachments'), 'wizard_id', 'attachment_id', 'Attachments'),
'history_mode': fields.selection([('info', 'Case Information'), 'history_mode': fields.selection([('info', 'Internal notes'),
('latest', 'Latest email'), ('whole', 'Whole Story')], ('latest', 'Latest email'), ('whole', 'Whole Story')],
'Send history', required=True), 'Send history', required=True),
} }
_defaults = { _defaults = {
'history_mode': 'latest', 'history_mode': 'info',
} }
def default_get(self, cr, uid, fields, context=None):
if context is None:
context = {}
# set as comment, perform overrided document-like action that calls get_record_data
old_mode = context.get('default_composition_mode', 'forward')
context['default_composition_mode'] = 'comment'
res = super(crm_lead_forward_to_partner, self).default_get(cr, uid, fields, context=context)
# back to forward mode
context['default_composition_mode'] = old_mode
res['composition_mode'] = context['default_composition_mode']
return res
def get_record_data(self, cr, uid, model, res_id, context=None): def get_record_data(self, cr, uid, model, res_id, context=None):
""" Override of mail.compose.message, to add default values coming """ Override of mail.compose.message, to add default values coming
form the related lead. form the related lead.
""" """
if context is None:
context = {}
res = super(crm_lead_forward_to_partner, self).get_record_data(cr, uid, model, res_id, context=context) res = super(crm_lead_forward_to_partner, self).get_record_data(cr, uid, model, res_id, context=context)
if model not in ('crm.lead') or not res_id: if model not in ('crm.lead') or not res_id:
return res return res
lead_obj = self.pool.get(model) template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm_partner_assign', 'crm_partner_assign_email_template')[1]
lead = lead_obj.browse(cr, uid, res_id, context=context) context['history_mode'] = context.get('history_mode','whole')
subject = '%s: %s - %s' % (_('Fwd'), _('Lead forward'), lead.name) mail_body_fields = ['partner_id', 'partner_name', 'title', 'function', 'street', 'street2', 'zip', 'city', 'country_id', 'state_id', 'email_from', 'phone', 'fax', 'mobile', 'description']
body = self._get_message_body(cr, uid, lead, 'info', context=context) lead = self.pool.get('crm.lead').browse(cr, uid, res_id, context=context)
res.update({ context['mail_body'] = self.pool.get('crm.lead')._mail_body(cr, uid, lead, mail_body_fields, context=context)
'subject': subject, template = self.generate_email_for_composer(cr, uid, template_id, res_id, context)
'body': body, res['subject'] = template['subject']
}) res['body'] = template['body']
return res return res
def on_change_history_mode(self, cr, uid, ids, history_mode, model, res_id, context=None): def on_change_history_mode(self, cr, uid, ids, history_mode, model, res_id, context=None):
""" Update body when changing history_mode """ """ Update body when changing history_mode """
if context is None:
context = {}
if model and model == 'crm.lead' and res_id: if model and model == 'crm.lead' and res_id:
lead = self.pool.get(model).browse(cr, uid, res_id, context=context) lead = self.pool.get(model).browse(cr, uid, res_id, context=context)
body = self._get_message_body(cr, uid, lead, history_mode, context=context) context['history_mode'] = history_mode
body = self.get_record_data(cr, uid, 'crm.lead', res_id, context=context)['body']
return {'value': {'body': body}} return {'value': {'body': body}}
def create(self, cr, uid, values, context=None): def create(self, cr, uid, values, context=None):
@ -116,56 +118,7 @@ class crm_lead_forward_to_partner(osv.osv_memory):
value = self.default_get(cr, uid, ['body', 'email_to', 'email_cc', 'subject', 'history_mode'], context=context) value = self.default_get(cr, uid, ['body', 'email_to', 'email_cc', 'subject', 'history_mode'], context=context)
self.write(cr, uid, ids, value, context=context) self.write(cr, uid, ids, value, context=context)
self.send_mail(cr, uid, ids, context=context) return self.send_mail(cr, uid, ids, context=context)
# for case in lead.browse(cr, uid, lead_ids, context=context):
# TODO: WHAT TO DO WITH THAT ?
# if (this.send_to == 'partner' and this.partner_id):
# lead.assign_partner(cr, uid, [case.id], this.partner_id.id, context=context)
# if this.send_to == 'user':
# lead.allocate_salesman(cr, uid, [case.id], [this.user_id.id], context=context)
return res
def _get_info_body(self, cr, uid, lead, context=None):
field_names = []
proxy = self.pool.get(lead._name)
if lead.type == 'opportunity':
field_names += ['partner_id']
field_names += [
'partner_name' , 'title', 'function', 'street', 'street2',
'zip', 'city', 'country_id', 'state_id', 'email_from',
'phone', 'fax', 'mobile', 'categ_id', 'description',
]
return proxy._mail_body(cr, uid, lead, field_names, context=context)
def _get_message_body(self, cr, uid, lead, history_mode='whole', context=None):
""" This function gets whole communication history and returns as top
posting style
#1: form a body, based on the lead
#2: append to the body the communication history, based on the
history_mode parameter
- info: Forward the case information
- latest: Send the latest history
- whole: Send the whole history
:param lead: browse_record on crm.lead
:param history_mode: 'whole' or 'latest'
"""
mail_message = self.pool.get('mail.message')
body = self._get_info_body(cr, uid, lead, context=context)
if history_mode not in ('whole', 'latest'):
return body or ''
for message in lead.message_ids:
header = '-------- Original Message --------'
sentdate = 'Date: %s' % (message.date or '')
desc = '\n%s'%(message.body)
original = [header, sentdate, desc, '\n']
original = '\n'.join(original)
body += original
if history_mode == 'latest':
break
return body or ''
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -15,8 +15,7 @@
<field name="history_mode" colspan="4" <field name="history_mode" colspan="4"
on_change="on_change_history_mode(history_mode, model, res_id)"/> on_change="on_change_history_mode(history_mode, model, res_id)"/>
<field name="subject" colspan="4"/> <field name="subject" colspan="4"/>
<field name="partner_ids" colspan="4" widget="many2many_tags" <field name="partner_ids" colspan="4" widget="many2many_tags_email"/>
on_change="onchange_partner_ids(partner_ids)"/>
<notebook colspan="4"> <notebook colspan="4">
<page string="Body"> <page string="Body">
<field name="body"/> <field name="body"/>

View File

@ -181,11 +181,11 @@ class contentIndex(object):
res = (mime, fobj.indexContent(content,filename,fname or realfname) ) res = (mime, fobj.indexContent(content,filename,fname or realfname) )
else: else:
_logger.debug("Have no object, return (%s, None).", mime) _logger.debug("Have no object, return (%s, None).", mime)
res = (mime, None ) res = (mime, '')
except Exception: except Exception:
_logger.exception("Cannot index file %s (%s).", _logger.exception("Cannot index file %s (%s).",
filename, fname or realfname) filename, fname or realfname)
res = None res = (mime, '')
# If we created a tmp file, unlink it now # If we created a tmp file, unlink it now
if not realfname and fname: if not realfname and fname:

View File

@ -105,7 +105,7 @@ class DocIndex(indexer):
_logger.warning("Failed attempt to execute antiword (MS Word reader). Antiword is necessary to index the file %s of MIME type %s. Detailed error available at DEBUG level.", fname, self._getMimeTypes()[0]) _logger.warning("Failed attempt to execute antiword (MS Word reader). Antiword is necessary to index the file %s of MIME type %s. Detailed error available at DEBUG level.", fname, self._getMimeTypes()[0])
_logger.debug("Trace of the failed file indexing attempt.", exc_info=True) _logger.debug("Trace of the failed file indexing attempt.", exc_info=True)
return False return u''
cntIndex.register(DocIndex()) cntIndex.register(DocIndex())
@ -166,9 +166,14 @@ class PdfIndex(indexer):
return ['.pdf'] return ['.pdf']
def _doIndexFile(self, fname): def _doIndexFile(self, fname):
pop = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', fname, '-'], shell=False, stdout=PIPE) try:
(data, _) = pop.communicate() pop = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', fname, '-'], shell=False, stdout=PIPE)
return _to_unicode(data) (data, _) = pop.communicate()
return _to_unicode(data)
except OSError:
_logger.warning("Failed attempt to execute pdftotext. This program is necessary to index the file %s of MIME type %s. Detailed error available at DEBUG level.", fname, self._getMimeTypes()[0])
_logger.debug("Trace of the failed file indexing attempt.", exc_info=True)
return u''
cntIndex.register(PdfIndex()) cntIndex.register(PdfIndex())

View File

@ -65,6 +65,7 @@ class email_template(osv.osv):
"Templates for sending email" "Templates for sending email"
_name = "email.template" _name = "email.template"
_description = 'Email Templates' _description = 'Email Templates'
_order = 'name'
def render_template(self, cr, uid, template, model, res_id, context=None): def render_template(self, cr, uid, template, model, res_id, context=None):
"""Render the given template text, replace mako expressions ``${expr}`` """Render the given template text, replace mako expressions ``${expr}``
@ -141,7 +142,9 @@ class email_template(osv.osv):
help="If checked, the user's signature will be appended to the text version " help="If checked, the user's signature will be appended to the text version "
"of the message"), "of the message"),
'subject': fields.char('Subject', translate=True, help="Subject (placeholders may be used here)",), 'subject': fields.char('Subject', translate=True, help="Subject (placeholders may be used here)",),
'email_from': fields.char('From', help="Sender address (placeholders may be used here)"), 'email_from': fields.char('From',
help="Sender address (placeholders may be used here). If not set, the default "
"value will be the author's email alias if configured, or email address."),
'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"), 'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"),
'email_recipients': fields.char('To (Partners)', help="Comma-separated ids of recipient partners (placeholders may be used here)"), 'email_recipients': fields.char('To (Partners)', help="Comma-separated ids of recipient partners (placeholders may be used here)"),
'email_cc': fields.char('Cc', help="Carbon copy recipients (placeholders may be used here)"), 'email_cc': fields.char('Cc', help="Carbon copy recipients (placeholders may be used here)"),

View File

@ -28,7 +28,7 @@
<page string="Email Details"> <page string="Email Details">
<group> <group>
<group string="Addressing"> <group string="Addressing">
<field name="email_from" required="1"/> <field name="email_from"/>
<field name="email_to"/> <field name="email_to"/>
<field name="email_recipients"/> <field name="email_recipients"/>
<field name="email_cc"/> <field name="email_cc"/>
@ -43,7 +43,7 @@
<field name="copyvalue"/> <field name="copyvalue"/>
</group> </group>
<group string="Contents" colspan="2"> <group string="Contents" colspan="2">
<field name="subject" required="1"/> <field name="subject"/>
<field name="body_html" width="250" height="450" nolabel="1" colspan="2" placeholder="Email contents (in raw HTML format)"/> <field name="body_html" width="250" height="450" nolabel="1" colspan="2" placeholder="Email contents (in raw HTML format)"/>
</group> </group>
</group> </group>

View File

@ -19,7 +19,7 @@
# #
############################################################################## ##############################################################################
from openerp.osv import fields,osv from openerp.osv import fields, osv
class res_partner(osv.osv): class res_partner(osv.osv):
"""Inherit res.partner to add a generic opt-out field that can be used """Inherit res.partner to add a generic opt-out field that can be used
@ -28,8 +28,9 @@ class res_partner(osv.osv):
_inherit = 'res.partner' _inherit = 'res.partner'
_columns = { _columns = {
'opt_out': fields.boolean('Opt-Out', help="If checked, this partner will not receive any automated email " \ 'opt_out': fields.boolean('Opt-Out',
"notifications, such as the availability of invoices."), help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
"Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing."),
} }
_defaults = { _defaults = {

View File

@ -11,5 +11,19 @@
</xpath> </xpath>
</field> </field>
</record> </record>
<record model="ir.ui.view" id="res_partner_opt_out_search">
<field name="name">res.partner.opt_out.search</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
<filter string="Suppliers" position="after">
<separator />
<filter string="Available for mass mailing"
name='not_opt_out' domain="[('opt_out', '=', False)]"
help="Partners that did not ask not to be included in mass mailing campaigns" />
</filter>
</field>
</record>
</data> </data>
</openerp> </openerp>

View File

@ -80,7 +80,7 @@ class mail_compose_message(osv.TransientModel):
'datas_fname': attach_fname, 'datas_fname': attach_fname,
'res_model': model, 'res_model': model,
'res_id': res_id, 'res_id': res_id,
'type': 'binary', # override default_type from context, possibly meant for another model! 'type': 'binary', # override default_type from context, possibly meant for another model!
} }
values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context)) values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context))
else: else:
@ -125,19 +125,22 @@ class mail_compose_message(osv.TransientModel):
fields = ['body_html', 'subject', 'email_to', 'email_recipients', 'email_cc', 'attachments'] fields = ['body_html', 'subject', 'email_to', 'email_recipients', 'email_cc', 'attachments']
values = dict((field, template_values[field]) for field in fields if template_values.get(field)) values = dict((field, template_values[field]) for field in fields if template_values.get(field))
values['body'] = values.pop('body_html', '') values['body'] = values.pop('body_html', '')
# transform email_to, email_cc into partner_ids
values['partner_ids'] = []
# transform email_to, email_cc into partner_ids
partner_ids = set()
mails = tools.email_split(values.pop('email_to', '') + ' ' + values.pop('email_cc', '')) mails = tools.email_split(values.pop('email_to', '') + ' ' + values.pop('email_cc', ''))
for mail in mails: for mail in mails:
partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context) partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context)
values['partner_ids'].append(partner_id) partner_ids.add(partner_id)
email_recipients = values.pop('email_recipients', '') email_recipients = values.pop('email_recipients', '')
if email_recipients: if email_recipients:
for partner_id in email_recipients.split(','): for partner_id in email_recipients.split(','):
values['partner_ids'].append(int(partner_id)) if partner_id: # placeholders could generate '', 3, 2 due to some empty field values
partner_ids.add(int(partner_id))
values['partner_ids'] = list(set(values['partner_ids'])) # legacy template behavior: void values do not erase existing values and the
# related key is removed from the values dict
if partner_ids:
values['partner_ids'] = list(partner_ids)
return values return values

View File

@ -168,7 +168,7 @@
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread" placeholder="Share a note..."/> <field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -405,7 +405,7 @@
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread" placeholder="Share a note..."/> <field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -78,7 +78,7 @@ class fetchmail_server(osv.osv):
'priority': fields.integer('Server Priority', readonly=True, states={'draft':[('readonly', False)]}, help="Defines the order of processing, " 'priority': fields.integer('Server Priority', readonly=True, states={'draft':[('readonly', False)]}, help="Defines the order of processing, "
"lower values mean higher priority"), "lower values mean higher priority"),
'message_ids': fields.one2many('mail.mail', 'fetchmail_server_id', 'Messages', readonly=True), 'message_ids': fields.one2many('mail.mail', 'fetchmail_server_id', 'Messages', readonly=True),
'configuration' : fields.text('Configuration'), 'configuration' : fields.text('Configuration', readonly=True),
'script' : fields.char('Script', readonly=True, size=64), 'script' : fields.char('Script', readonly=True, size=64),
} }
_defaults = { _defaults = {
@ -113,7 +113,16 @@ class fetchmail_server(osv.osv):
conf['model']=r[0]['model'] conf['model']=r[0]['model']
values['configuration'] = """Use the below script with the following command line options with your Mail Transport Agent (MTA) values['configuration'] = """Use the below script with the following command line options with your Mail Transport Agent (MTA)
openerp_mailgate.py -u %(uid)d -p PASSWORD -o %(model)s -d %(dbname)s --host=HOSTNAME --port=PORT openerp_mailgate.py --host=HOSTNAME --port=PORT -u %(uid)d -p PASSWORD -d %(dbname)s
Example configuration for the postfix mta running locally:
/etc/postfix/virtual_aliases:
@youdomain openerp_mailgate@localhost
/etc/aliases:
openerp_mailgate: "|/path/to/openerp-mailgate.py --host=localhost -u %(uid)d -p PASSWORD -d %(dbname)s"
""" % conf """ % conf
return {'value':values} return {'value':values}

View File

@ -160,7 +160,7 @@
<button name="button_final_validation" string="Validate Appraisal" states="wait" type="object" class="oe_highlight"/> <button name="button_final_validation" string="Validate Appraisal" states="wait" type="object" class="oe_highlight"/>
<button name="button_done" string="Done" states="progress" type="object" class="oe_highlight"/> <button name="button_done" string="Done" states="progress" type="object" class="oe_highlight"/>
<button name="button_draft" string="Reset to Draft" states="cancel" type="object" /> <button name="button_draft" string="Reset to Draft" states="cancel" type="object" />
<button name="button_cancel" string="Cancel" states="draft,wait,progress" type="object"/> <button name="button_cancel" string="Cancel Appraisal" states="draft,wait,progress" type="object"/>
<field name="state" widget="statusbar" statusbar_visible="draft,progress,wait,done" statusbar_colors='{"progress":"blue"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,progress,wait,done" statusbar_colors='{"progress":"blue"}'/>
</header> </header>
<sheet> <sheet>
@ -282,7 +282,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Interview Appraisal" version="7.0"> <form string="Interview Appraisal" version="7.0">
<header> <header>
<button string="Cancel" name="survey_req_cancel" type="object" <button string="Cancel Survey" name="survey_req_cancel" type="object"
states="draft,waiting_answer" class="oe_left"/> states="draft,waiting_answer" class="oe_left"/>
<button string="Print Survey" name="action_print_survey" type="object" <button string="Print Survey" name="action_print_survey" type="object"
states="draft" context="{'survey_id': survey_id, 'response_id': [response], 'response_no':0}" states="draft" context="{'survey_id': survey_id, 'response_id': [response], 'response_no':0}"

View File

@ -39,14 +39,14 @@ The whole flow is implemented as:
* Draft expense * Draft expense
* Confirmation of the sheet by the employee * Confirmation of the sheet by the employee
* Validation by his manager * Validation by his manager
* Validation by the accountant and receipt creation * Validation by the accountant and accounting entries creation
This module also uses analytic accounting and is compatible with the invoice on timesheet module so that you are able to automatically re-invoice your customers' expenses if your work by project. This module also uses analytic accounting and is compatible with the invoice on timesheet module so that you are able to automatically re-invoice your customers' expenses if your work by project.
""", """,
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
'images': ['images/hr_expenses_analysis.jpeg', 'images/hr_expenses.jpeg'], 'images': ['images/hr_expenses_analysis.jpeg', 'images/hr_expenses.jpeg'],
'depends': ['hr', 'account_voucher'], 'depends': ['hr', 'account_accountant'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'hr_expense_data.xml', 'hr_expense_data.xml',

View File

@ -144,92 +144,233 @@ class hr_expense_expense(osv.osv):
def expense_canceled(self, cr, uid, ids, context=None): def expense_canceled(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context) return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
def action_receipt_create(self, cr, uid, ids, context=None): def account_move_get(self, cr, uid, expense_id, context=None):
property_obj = self.pool.get('ir.property') '''
sequence_obj = self.pool.get('ir.sequence') This method prepare the creation of the account move related to the given expense.
analytic_journal_obj = self.pool.get('account.analytic.journal')
account_journal = self.pool.get('account.journal') :param expense_id: Id of voucher for which we are creating account_move.
voucher_obj = self.pool.get('account.voucher') :return: mapping between fieldname and value of account move to create
currency_obj = self.pool.get('res.currency') :rtype: dict
'''
journal_obj = self.pool.get('account.journal')
expense = self.browse(cr, uid, expense_id, context=context)
company_id = expense.company_id.id
date = expense.date_confirm
ref = expense.name
journal_id = False
if expense.journal_id:
journal_id = expense.journal_id.id
else:
journal_id = journal_obj.search(cr, uid, [('type', '=', 'purchase'), ('company_id', '=', company_id)])
if not journal_id:
raise osv.except_osv(_('Error!'), _("No expense journal found. Please make sure you have a journal with type 'purchase' configured."))
journal_id = journal_id[0]
return self.pool.get('account.move').account_move_prepare(cr, uid, journal_id, date=date, ref=ref, company_id=company_id, context=context)
def line_get_convert(self, cr, uid, x, part, date, context=None):
partner_id = self.pool.get('res.partner')._find_accounting_partner(part).id
return {
'date_maturity': x.get('date_maturity', False),
'partner_id': partner_id,
'name': x['name'][:64],
'date': date,
'debit': x['price']>0 and x['price'],
'credit': x['price']<0 and -x['price'],
'account_id': x['account_id'],
'analytic_lines': x.get('analytic_lines', False),
'amount_currency': x['price']>0 and abs(x.get('amount_currency', False)) or -abs(x.get('amount_currency', False)),
'currency_id': x.get('currency_id', False),
'tax_code_id': x.get('tax_code_id', False),
'tax_amount': x.get('tax_amount', False),
'ref': x.get('ref', False),
'quantity': x.get('quantity',1.00),
'product_id': x.get('product_id', False),
'product_uom_id': x.get('uos_id', False),
'analytic_account_id': x.get('account_analytic_id', False),
}
def compute_expense_totals(self, cr, uid, exp, company_currency, ref, account_move_lines, context=None):
'''
internal method used for computation of total amount of an expense in the company currency and
in the expense currency, given the account_move_lines that will be created. It also do some small
transformations at these account_move_lines (for multi-currency purposes)
:param account_move_lines: list of dict
:rtype: tuple of 3 elements (a, b ,c)
a: total in company currency
b: total in hr.expense currency
c: account_move_lines potentially modified
'''
cur_obj = self.pool.get('res.currency')
if context is None: if context is None:
context = {} context={}
for exp in self.browse(cr, uid, ids, context=context): context.update({'date': exp.date_confirm or time.strftime('%Y-%m-%d')})
company_id = exp.company_id.id total = 0.0
lines = [] total_currency = 0.0
total = 0.0 for i in account_move_lines:
ctx = context.copy() if exp.currency_id.id != company_currency:
ctx.update({'date': exp.date}) i['currency_id'] = exp.currency_id.id
journal = False i['amount_currency'] = i['price']
if exp.journal_id: i['price'] = cur_obj.compute(cr, uid, exp.currency_id.id,
journal = exp.journal_id company_currency, i['price'],
context=context)
else: else:
journal_id = voucher_obj._get_journal(cr, uid, context={'type': 'purchase', 'company_id': company_id}) i['amount_currency'] = False
if journal_id: i['currency_id'] = False
journal = account_journal.browse(cr, uid, journal_id, context=context) total -= i['price']
if not journal: total_currency -= i['amount_currency'] or i['price']
raise osv.except_osv(_('Error!'), _("No expense journal found. Please make sure you have a journal with type 'purchase' configured.")) return total, total_currency, account_move_lines
for line in exp.line_ids:
if line.product_id: def action_move_create(self, cr, uid, ids, context=None):
acc = line.product_id.property_account_expense '''
if not acc: main function that is called when trying to create the accounting entries related to an expense
acc = line.product_id.categ_id.property_account_expense_categ '''
else: move_obj = self.pool.get('account.move')
acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company_id}) for exp in self.browse(cr, uid, ids, context=context):
if not acc:
raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
total_amount = line.total_amount
if journal.currency:
if exp.currency_id != journal.currency:
total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, journal.currency.id, total_amount, context=ctx)
elif exp.currency_id != exp.company_id.currency_id:
total_amount = currency_obj.compute(cr, uid, exp.currency_id.id, exp.company_id.currency_id.id, total_amount, context=ctx)
lines.append((0, False, {
'name': line.name,
'account_id': acc.id,
'account_analytic_id': line.analytic_account.id,
'amount': total_amount,
'type': 'dr'
}))
total += total_amount
if not exp.employee_id.address_home_id: if not exp.employee_id.address_home_id:
raise osv.except_osv(_('Error!'), _('The employee must have a home address.')) raise osv.except_osv(_('Error!'), _('The employee must have a home address.'))
company_currency = exp.company_id.currency_id.id
diff_currency_p = exp.currency_id.id <> company_currency
#create the move that will contain the accounting entries
move_id = move_obj.create(cr, uid, self.account_move_get(cr, uid, exp.id, context=context), context=context)
#one account.move.line per expense line (+taxes..)
eml = self.move_line_get(cr, uid, exp.id, context=context)
#create one more move line, a counterline for the total on payable account
total, total_currency, eml = self.compute_expense_totals(cr, uid, exp, company_currency, exp.name, eml, context=context)
acc = exp.employee_id.address_home_id.property_account_payable.id acc = exp.employee_id.address_home_id.property_account_payable.id
voucher = { eml.append({
'name': exp.name or '/', 'type': 'dest',
'reference': sequence_obj.get(cr, uid, 'hr.expense.invoice'), 'name': '/',
'account_id': acc, 'price': total,
'type': 'purchase', 'account_id': acc,
'partner_id': exp.employee_id.address_home_id.id, 'date_maturity': exp.date_confirm,
'company_id': company_id, 'amount_currency': diff_currency_p and total_currency or False,
'line_ids': lines, 'currency_id': diff_currency_p and exp.currency_id.id or False,
'amount': total, 'ref': exp.name
'journal_id': journal.id, })
}
if journal and not journal.analytic_journal_id: #convert eml into an osv-valid format
analytic_journal_ids = analytic_journal_obj.search(cr, uid, [('type','=','purchase')], context=context) lines = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, exp.employee_id.address_home_id, exp.date_confirm, context=context)), eml)
if analytic_journal_ids: move_obj.write(cr, uid, [move_id], {'line_id': lines}, context=context)
account_journal.write(cr, uid, [journal.id], {'analytic_journal_id': analytic_journal_ids[0]}, context=context) self.write(cr, uid, ids, {'account_move_id': move_id, 'state': 'done'}, context=context)
voucher_id = voucher_obj.create(cr, uid, voucher, context=context)
self.write(cr, uid, [exp.id], {'voucher_id': voucher_id, 'state': 'done'}, context=context)
return True return True
def action_view_receipt(self, cr, uid, ids, context=None): def move_line_get(self, cr, uid, expense_id, context=None):
res = []
tax_obj = self.pool.get('account.tax')
cur_obj = self.pool.get('res.currency')
if context is None:
context = {}
exp = self.browse(cr, uid, expense_id, context=context)
company_currency = exp.company_id.currency_id.id
for line in exp.line_ids:
mres = self.move_line_get_item(cr, uid, line, context)
if not mres:
continue
res.append(mres)
tax_code_found= False
#Calculate tax according to default tax on product
taxes = []
#Taken from product_id_onchange in account.invoice
if line.product_id:
fposition_id = False
fpos_obj = self.pool.get('account.fiscal.position')
fpos = fposition_id and fpos_obj.browse(cr, uid, fposition_id, context=context) or False
product = line.product_id
taxes = product.supplier_taxes_id
#If taxes are not related to the product, maybe they are in the account
if not taxes:
a = product.property_account_expense.id #Why is not there a check here?
if not a:
a = product.categ_id.property_account_expense_categ.id
a = fpos_obj.map_account(cr, uid, fpos, a)
taxes = a and self.pool.get('account.account').browse(cr, uid, a, context=context).tax_ids or False
tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes)
if not taxes:
continue
#Calculating tax on the line and creating move?
for tax in tax_obj.compute_all(cr, uid, taxes,
line.unit_amount ,
line.unit_quantity, line.product_id,
exp.user_id.partner_id)['taxes']:
tax_code_id = tax['base_code_id']
tax_amount = line.total_amount * tax['base_sign']
if tax_code_found:
if not tax_code_id:
continue
res.append(self.move_line_get_item(cr, uid, line, context))
res[-1]['price'] = 0.0
res[-1]['account_analytic_id'] = False
elif not tax_code_id:
continue
tax_code_found = True
res[-1]['tax_code_id'] = tax_code_id
res[-1]['tax_amount'] = cur_obj.compute(cr, uid, exp.currency_id.id, company_currency, tax_amount, context={'date': exp.date_confirm})
#Will create the tax here as we don't have the access
assoc_tax = {
'type':'tax',
'name':tax['name'],
'price_unit': tax['price_unit'],
'quantity': 1,
'price': tax['amount'] * tax['base_sign'] or 0.0,
'account_id': tax['account_collected_id'],
'tax_code_id': tax['tax_code_id'],
'tax_amount': tax['amount'] * tax['base_sign'],
}
res.append(assoc_tax)
return res
def move_line_get_item(self, cr, uid, line, context=None):
company = line.expense_id.company_id
property_obj = self.pool.get('ir.property')
if line.product_id:
acc = line.product_id.property_account_expense
if not acc:
acc = line.product_id.categ_id.property_account_expense_categ
else:
acc = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context={'force_company': company.id})
if not acc:
raise osv.except_osv(_('Error!'), _('Please configure Default Expense account for Product purchase: `property_account_expense_categ`.'))
return {
'type':'src',
'name': line.name.split('\n')[0][:64],
'price_unit':line.unit_amount,
'quantity':line.unit_quantity,
'price':line.total_amount,
'account_id':acc.id,
'product_id':line.product_id.id,
'uos_id':line.uom_id.id,
'account_analytic_id':line.analytic_account.id,
}
def action_view_move(self, cr, uid, ids, context=None):
''' '''
This function returns an action that display existing receipt of given expense ids. This function returns an action that display existing account.move of given expense ids.
''' '''
assert len(ids) == 1, 'This option should only be used for a single id at a time' assert len(ids) == 1, 'This option should only be used for a single id at a time'
voucher_id = self.browse(cr, uid, ids[0], context=context).voucher_id.id expense = self.browse(cr, uid, ids[0], context=context)
res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account_voucher', 'view_purchase_receipt_form') assert expense.account_move_id
try:
dummy, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'view_move_form')
except ValueError, e:
view_id = False
result = { result = {
'name': _('Expense Receipt'), 'name': _('Expense Account Move'),
'view_type': 'form', 'view_type': 'form',
'view_mode': 'form', 'view_mode': 'form',
'view_id': res and res[1] or False, 'view_id': view_id,
'res_model': 'account.voucher', 'res_model': 'account.move',
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'nodestroy': True, 'nodestroy': True,
'target': 'current', 'target': 'current',
'res_id': voucher_id, 'res_id': expense.account_move_id.id,
} }
return result return result

View File

@ -67,7 +67,7 @@
<button name="refuse" states="confirm,accepted" string="Refuse" type="workflow" groups="base.group_hr_user" /> <button name="refuse" states="confirm,accepted" string="Refuse" type="workflow" groups="base.group_hr_user" />
<button name="draft" states="confirm,cancelled" string="Set to Draft" type="workflow" groups="base.group_hr_user" /> <button name="draft" states="confirm,cancelled" string="Set to Draft" type="workflow" groups="base.group_hr_user" />
<button name="done" states="accepted" string="Generate Accounting Entries" type="workflow" groups="account.group_account_invoice" class="oe_highlight"/> <button name="done" states="accepted" string="Generate Accounting Entries" type="workflow" groups="account.group_account_invoice" class="oe_highlight"/>
<button name="action_view_receipt" states="done" string="Open Receipt" type="object"/> <button name="action_view_move" states="done" string="Open Accounting Entries" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,accepted,done" statusbar_colors='{"confirm":"blue","cancelled":"red"}'/> <field name="state" widget="statusbar" statusbar_visible="draft,confirm,accepted,done" statusbar_colors='{"confirm":"blue","cancelled":"red"}'/>
</header> </header>
<sheet> <sheet>
@ -133,7 +133,7 @@
<group> <group>
<group string="Accounting Data"> <group string="Accounting Data">
<field name="journal_id" widget="selection" domain="[('type', '=', 'purchase')]"/> <field name="journal_id" widget="selection" domain="[('type', '=', 'purchase')]"/>
<field name="voucher_id" context="{'form_view_ref': 'account_voucher.view_purchase_receipt_form'}"/> <field name="account_move_id"/>
</group> </group>
</group> </group>
</page> </page>

View File

@ -43,7 +43,7 @@
<field name="wkf_id" ref="wkf_expenses"/> <field name="wkf_id" ref="wkf_expenses"/>
<field name="name">done</field> <field name="name">done</field>
<field name="kind">function</field> <field name="kind">function</field>
<field name="action">action_receipt_create()</field> <field name="action">action_move_create()</field>
</record> </record>
<record id="t1" model="workflow.transition"> <record id="t1" model="workflow.transition">

View File

@ -26,11 +26,6 @@
!python {model: hr.expense.expense}: | !python {model: hr.expense.expense}: |
sep_expenses = self.browse(cr, uid, ref("sep_expenses"), context=context) sep_expenses = self.browse(cr, uid, ref("sep_expenses"), context=context)
assert sep_expenses.state == 'done', "Expense should be in 'Done' state." assert sep_expenses.state == 'done', "Expense should be in 'Done' state."
assert sep_expenses.voucher_id, "Expense should have link of Purchase Receipt."
assert sep_expenses.voucher_id.type == 'purchase', "Receipt type is not purchase receipt."
assert sep_expenses.voucher_id.amount == sep_expenses.amount,"Receipt total amount is not correspond with expense total."
assert len(sep_expenses.voucher_id.line_dr_ids) == len(sep_expenses.line_ids),"Lines of Receipt and expense line are not correspond."
- -
I duplicate the expenses and cancel duplicated. I duplicate the expenses and cancel duplicated.
- -

View File

@ -217,7 +217,7 @@
<button string="Refund" name="refund_sheet" states="confirm,done" type='object' /> <button string="Refund" name="refund_sheet" states="confirm,done" type='object' />
<button string="Set to Draft" name="draft" states="cancel"/> <button string="Set to Draft" name="draft" states="cancel"/>
<button string="Compute Sheet" name="compute_sheet" type="object" states="draft" class="oe_highlight"/> <button string="Compute Sheet" name="compute_sheet" type="object" states="draft" class="oe_highlight"/>
<button string="Cancel" name="cancel_sheet" states="draft,hr_check,confirm,verify"/> <button string="Cancel Payslip" name="cancel_sheet" states="draft,hr_check,confirm,verify"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/> <field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
</header> </header>
<sheet> <sheet>

View File

@ -340,6 +340,15 @@ class hr_applicant(base_stage, osv.Model):
value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context) value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
return value return value
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(hr_applicant, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for applicant in self.browse(cr, uid, ids, context=context):
if applicant.partner_id:
self._message_add_suggested_recipient(cr, uid, recipients, applicant, partner=applicant.partner_id, reason=_('Contact'))
elif applicant.email_from:
self._message_add_suggested_recipient(cr, uid, recipients, applicant, email=applicant.email_from, reason=_('Contact Email'))
return recipients
def message_new(self, cr, uid, msg, custom_values=None, context=None): def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway """ Overrides mail_thread message_new that is called by the mailgateway
through message_process. through message_process.

View File

@ -151,7 +151,7 @@
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread" placeholder="Share a message..."/> <field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -237,6 +237,7 @@
<field name="title_action"/> <field name="title_action"/>
<field name="department_id"/> <field name="department_id"/>
<field name="categ_ids"/> <field name="categ_ids"/>
<field name="message_summary"/>
<templates> <templates>
<t t-name="kanban-tooltip"> <t t-name="kanban-tooltip">
<ul class="oe_kanban_tooltip"> <ul class="oe_kanban_tooltip">
@ -282,6 +283,7 @@
</div> </div>
<div class="oe_kanban_footer_left" style="margin-top:5px;"> <div class="oe_kanban_footer_left" style="margin-top:5px;">
<t t-raw="record.message_summary.raw_value"/>
<field name="categ_ids"/> <field name="categ_ids"/>
</div> </div>
</div> </div>

View File

@ -348,10 +348,10 @@
<para style="terp_default_7">[[ p.total or '' ]]</para> <para style="terp_default_7">[[ p.total or '' ]]</para>
</td> </td>
<td> <td>
<para style="terp_default_7">[[ get_employer_line(o, p).rate or '']] </para> <para style="terp_default_7">[[ get_employer_line(o, p) and get_employer_line(o, p).rate or '']] </para>
</td> </td>
<td> <td>
<para style="terp_default_7">[[ get_employer_line(o,p).total or '' ]]</para> <para style="terp_default_7">[[ get_employer_line(o,p) and get_employer_line(o,p).total or '' ]]</para>
</td> </td>
</tr> </tr>
</blockTable> </blockTable>

View File

@ -80,7 +80,7 @@
<header> <header>
<button name="confirm_sheet" string="Confirm Sheet" states="draft" type="object" class="oe_highlight"/> <button name="confirm_sheet" string="Confirm Sheet" states="draft" type="object" class="oe_highlight"/>
<button name="compute_advice" string="Compute Advice" states="draft" type="object" class="oe_highlight"/> <button name="compute_advice" string="Compute Advice" states="draft" type="object" class="oe_highlight"/>
<button name="cancel_sheet" string="Cancel" states="draft" type="object"/> <button name="cancel_sheet" string="Cancel Advice" states="draft" type="object"/>
<button name="set_to_draft" string="Set to Draft" states="cancel,confirm" type="object"/> <button name="set_to_draft" string="Set to Draft" states="cancel,confirm" type="object"/>
<div class="oe_right"> <div class="oe_right">
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft"/> <field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft"/>

View File

@ -2,9 +2,6 @@
<openerp> <openerp>
<data noupdate="1"> <data noupdate="1">
<record id="base.user_root" model="res.users">
<field name="groups_id" eval="[(4,ref('lunch.group_lunch_manager'))]"/>
</record>
<record id="base.user_demo" model="res.users"> <record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4,ref('lunch.group_lunch_user'))]"/> <field name="groups_id" eval="[(4,ref('lunch.group_lunch_user'))]"/>
</record> </record>

View File

@ -283,7 +283,7 @@
<field name="name">Order lines Tree</field> <field name="name">Order lines Tree</field>
<field name="model">lunch.order.line</field> <field name="model">lunch.order.line</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Order lines Tree"> <tree string="Order lines Tree" create="false" edit="false">
<field name='date'/> <field name='date'/>
<field name='user_id'/> <field name='user_id'/>
<field name='supplier' invisible='1'/> <field name='supplier' invisible='1'/>

View File

@ -14,6 +14,7 @@
<field name="name">Manager</field> <field name="name">Manager</field>
<field name="implied_ids" eval="[(4, ref('group_lunch_user'))]"/> <field name="implied_ids" eval="[(4, ref('group_lunch_user'))]"/>
<field name="category_id" ref="module_lunch_category"/> <field name="category_id" ref="module_lunch_category"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record> </record>
</data> </data>
</openerp> </openerp>

View File

@ -75,20 +75,25 @@ class mail_notification(osv.Model):
if not cr.fetchone(): if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)') cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
def get_partners_to_notify(self, cr, uid, message, context=None): def get_partners_to_notify(self, cr, uid, message, partners_to_notify=None, context=None):
""" Return the list of partners to notify, based on their preferences. """ Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify :param browse_record message: mail.message to notify
:param list partners_to_notify: optional list of partner ids restricting
the notifications to process
""" """
notify_pids = [] notify_pids = []
for notification in message.notification_ids: for notification in message.notification_ids:
if notification.read: if notification.read:
continue continue
partner = notification.partner_id partner = notification.partner_id
# If partners_to_notify specified: restrict to them
if partners_to_notify and partner.id not in partners_to_notify:
continue
# Do not send to partners without email address defined # Do not send to partners without email address defined
if not partner.email: if not partner.email:
continue continue
# Partner does not want to receive any emails # Partner does not want to receive any emails or is opt-out
if partner.notification_email_send == 'none': if partner.notification_email_send == 'none':
continue continue
# Partner wants to receive only emails and comments # Partner wants to receive only emails and comments
@ -100,16 +105,35 @@ class mail_notification(osv.Model):
notify_pids.append(partner.id) notify_pids.append(partner.id)
return notify_pids return notify_pids
def _notify(self, cr, uid, msg_id, context=None): def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None):
""" Send by email the notification depending on the user preferences """ """ Send by email the notification depending on the user preferences
:param list partners_to_notify: optional list of partner ids restricting
the notifications to process
"""
if context is None: if context is None:
context = {} context = {}
mail_message_obj = self.pool.get('mail.message')
# optional list of partners to notify: subscribe them if not already done or update the notification
if partners_to_notify:
notifications_to_update = []
notified_partners = []
notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', msg_id), ('partner_id', 'in', partners_to_notify)], context=context)
for notification in self.browse(cr, SUPERUSER_ID, notif_ids, context=context):
notified_partners.append(notification.partner_id.id)
notifications_to_update.append(notification.id)
partners_to_notify = filter(lambda item: item not in notified_partners, partners_to_notify)
if notifications_to_update:
self.write(cr, SUPERUSER_ID, notifications_to_update, {'read': False}, context=context)
mail_message_obj.write(cr, uid, msg_id, {'notified_partner_ids': [(4, id) for id in partners_to_notify]}, context=context)
# mail_notify_noemail (do not send email) or no partner_ids: do not send, return # mail_notify_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_notify_noemail'): if context.get('mail_notify_noemail'):
return True return True
# browse as SUPERUSER_ID because of access to res_partner not necessarily allowed # browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context) msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context)
notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, context=context) notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, partners_to_notify=partners_to_notify, context=context)
if not notify_partner_ids: if not notify_partner_ids:
return True return True
@ -136,16 +160,12 @@ class mail_notification(osv.Model):
mail_values = { mail_values = {
'mail_message_id': msg.id, 'mail_message_id': msg.id,
'email_to': [],
'auto_delete': True, 'auto_delete': True,
'body_html': body_html, 'body_html': body_html,
'email_from': email_from, 'email_from': email_from,
'state': 'outgoing',
} }
mail_values['email_to'] = ', '.join(mail_values['email_to'])
email_notif_id = mail_mail.create(cr, uid, mail_values, context=context) email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
try: try:
return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context) return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context)
except Exception: except Exception:
return False return False

View File

@ -106,7 +106,7 @@
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}' placeholder="Send a message to the group..."/> <field name="message_ids" widget="mail_thread" options='{"thread_level": 1}' placeholder="Send a message to the group"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -191,20 +191,31 @@ class mail_mail(osv.Model):
return body return body
def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None): def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method """ Return a specific ir_email reply_to.
is to be inherited by Portal, to add a link for signing in, in
each notification email a partner receives.
:param browse_record mail: mail.mail browse_record :param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner :param browse_record partner: specific recipient partner
""" """
if mail.reply_to: if mail.reply_to:
return mail.reply_to return mail.reply_to
if not mail.model or not mail.res_id: email_reply_to = False
return False
if not hasattr(self.pool.get(mail.model), 'message_get_reply_to'): # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
return False if mail.model and mail.res_id and hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
return self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0] email_reply_to = self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
# no alias reply_to -> reply_to will be the email_from, only the email part
if not email_reply_to and mail.email_from:
emails = tools.email_split(mail.email_from)
if emails:
email_reply_to = emails[0]
# format 'Document name <email_address>'
if email_reply_to and mail.model and mail.res_id:
document_name = self.pool.get(mail.model).name_get(cr, SUPERUSER_ID, [mail.res_id], context=context)[0]
if document_name:
email_reply_to = _('Followers of %s <%s>') % (document_name[1], email_reply_to)
return email_reply_to
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
""" Return a dictionary for specific email values, depending on a """ Return a dictionary for specific email values, depending on a

View File

@ -75,7 +75,7 @@
<field name="type" invisible="1"/> <field name="type" invisible="1"/>
<button name="send" string="Send Now" type="object" icon="gtk-media-play" states='outgoing'/> <button name="send" string="Send Now" type="object" icon="gtk-media-play" states='outgoing'/>
<button name="mark_outgoing" string="Retry" type="object" icon="gtk-redo" states='exception,cancel'/> <button name="mark_outgoing" string="Retry" type="object" icon="gtk-redo" states='exception,cancel'/>
<button name="cancel" string="Cancel" type="object" icon="terp-gtk-stop" states='outgoing'/> <button name="cancel" string="Cancel Email" type="object" icon="terp-gtk-stop" states='outgoing'/>
</tree> </tree>
</field> </field>
</record> </record>
@ -102,11 +102,10 @@
<field name="res_id"/> <field name="res_id"/>
</group> </group>
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="Status" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/> <filter string="Status" name="status" domain="[]" context="{'group_by':'state'}"/>
<filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/> <filter string="Author" name="author" context="{'group_by':'author_id'}"/>
<filter string="User" name="User" icon="terp-personal" context="{'group_by':'user_id'}"/> <filter string="Thread" name="thread" domain="[]" context="{'group_by':'message_id'}"/>
<filter string="Thread" icon="terp-mail-" domain="[]" context="{'group_by':'message_id'}"/> <filter string="Month" name="month" help="Creation Month" domain="[]" context="{'group_by':'date'}"/>
<filter string="Month" help="Creation Month" icon="terp-go-month" domain="[]" context="{'group_by':'date'}"/>
</group> </group>
</search> </search>
</field> </field>

View File

@ -301,8 +301,8 @@ class mail_message(osv.Model):
for key, message in message_tree.iteritems(): for key, message in message_tree.iteritems():
if message.author_id: if message.author_id:
partner_ids |= set([message.author_id.id]) partner_ids |= set([message.author_id.id])
if message.partner_ids: if message.notified_partner_ids:
partner_ids |= set([partner.id for partner in message.partner_ids]) partner_ids |= set([partner.id for partner in message.notified_partner_ids])
if message.attachment_ids: if message.attachment_ids:
attachment_ids |= set([attachment.id for attachment in message.attachment_ids]) attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
# Read partners as SUPERUSER -> display the names like classic m2o even if no access # Read partners as SUPERUSER -> display the names like classic m2o even if no access
@ -322,7 +322,7 @@ class mail_message(osv.Model):
else: else:
author = (0, message.email_from) author = (0, message.email_from)
partner_ids = [] partner_ids = []
for partner in message.partner_ids: for partner in message.notified_partner_ids:
if partner.id in partner_tree: if partner.id in partner_tree:
partner_ids.append(partner_tree[partner.id]) partner_ids.append(partner_tree[partner.id])
attachment_ids = [] attachment_ids = []
@ -861,7 +861,7 @@ class mail_message(osv.Model):
# message has no subtype_id: pure log message -> no partners, no one notified # message has no subtype_id: pure log message -> no partners, no one notified
if not message.subtype_id: if not message.subtype_id:
return True return True
# all followers of the mail.message document have to be added as partners and notified # all followers of the mail.message document have to be added as partners and notified
if message.model and message.res_id: if message.model and message.res_id:
fol_obj = self.pool.get("mail.followers") fol_obj = self.pool.get("mail.followers")
@ -884,8 +884,7 @@ class mail_message(osv.Model):
# notify # notify
if partners_to_notify: if partners_to_notify:
self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context) notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context)
notification_obj._notify(cr, uid, newid, context=context)
message.refresh() message.refresh()
# An error appear when a user receive a notification without notifying # An error appear when a user receive a notification without notifying

View File

@ -59,12 +59,12 @@
<field name="type"/> <field name="type"/>
<field name="author_id"/> <field name="author_id"/>
<field name="partner_ids"/> <field name="partner_ids"/>
<filter string="Read" <filter string="To Read"
name="message_read" help="Show messages to read"
domain="[('to_read', '=', False)]"/>
<filter string="Unread"
name="message_unread" help="Show messages to read" name="message_unread" help="Show messages to read"
domain="[('to_read', '=', True)]"/> domain="[('to_read', '=', True)]"/>
<filter string="Read"
name="message_read" help="Show already read messages"
domain="[('to_read', '=', False)]"/>
<separator/> <separator/>
<filter string="Comments" <filter string="Comments"
name="comments" help="Comments" name="comments" help="Comments"

View File

@ -93,7 +93,7 @@ class mail_thread(osv.AbstractModel):
""" Computes: """ Computes:
- message_unread: has uid unread message for the document - message_unread: has uid unread message for the document
- message_summary: html snippet summarizing the Chatter for kanban views """ - message_summary: html snippet summarizing the Chatter for kanban views """
res = dict((id, dict(message_unread=False, message_summary='')) for id in ids) res = dict((id, dict(message_unread=False, message_unread_count=0, message_summary=' ')) for id in ids)
user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
# search for unread messages, directly in SQL to improve performances # search for unread messages, directly in SQL to improve performances
@ -102,14 +102,14 @@ class mail_thread(osv.AbstractModel):
ON (n.message_id = m.id AND n.partner_id = %s AND (n.read = False or n.read IS NULL)) ON (n.message_id = m.id AND n.partner_id = %s AND (n.read = False or n.read IS NULL))
WHERE m.model = %s AND m.res_id in %s""", WHERE m.model = %s AND m.res_id in %s""",
(user_pid, self._name, tuple(ids),)) (user_pid, self._name, tuple(ids),))
msg_ids = [result[0] for result in cr.fetchall()] for result in cr.fetchall():
for msg_id in msg_ids: res[result[0]]['message_unread'] = True
res[msg_id]['message_unread'] = True res[result[0]]['message_unread_count'] += 1
for thread in self.browse(cr, uid, ids, context=context):
cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or ''
res[thread.id]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread.message_ids), len(thread.message_follower_ids))
for id in ids:
if res[id]['message_unread_count']:
title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New"))
return res return res
def _get_subscription_data(self, cr, uid, ids, name, args, context=None): def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
@ -607,15 +607,12 @@ class mail_thread(osv.AbstractModel):
if thread_id and hasattr(model_pool, 'message_update'): if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx) model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else: else:
nosub_ctx = dict(nosub_ctx, mail_create_nolog=True)
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx) thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else: else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread') model_pool = self.pool.get('mail.thread')
new_msg_id = model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg) new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
# when posting an incoming email to a document: subscribe the author, if a partner, as follower
if model and thread_id and msg.get('author_id'):
model_pool.message_subscribe(cr, uid, [thread_id], [msg.get('author_id')], context=context)
if partner_ids: if partner_ids:
# postponed after message_post, because this is an external message and we don't want to create # postponed after message_post, because this is an external message and we don't want to create
@ -760,7 +757,7 @@ class mail_thread(osv.AbstractModel):
_logger.debug('Parsing Message without message-id, generating a random one: %s', message_id) _logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
msg_dict['message_id'] = message_id msg_dict['message_id'] = message_id
if 'Subject' in message: if message.get('Subject'):
msg_dict['subject'] = decode(message.get('Subject')) msg_dict['subject'] = decode(message.get('Subject'))
# Envelope fields not stored in mail.message but made available for message_new() # Envelope fields not stored in mail.message but made available for message_new()
@ -768,16 +765,15 @@ class mail_thread(osv.AbstractModel):
msg_dict['to'] = decode(message.get('to')) msg_dict['to'] = decode(message.get('to'))
msg_dict['cc'] = decode(message.get('cc')) msg_dict['cc'] = decode(message.get('cc'))
if 'From' in message: if message.get('From'):
author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context) author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
if author_ids: if author_ids:
msg_dict['author_id'] = author_ids[0] msg_dict['author_id'] = author_ids[0]
else: msg_dict['email_from'] = decode(message.get('from'))
msg_dict['email_from'] = decode(message.get('from'))
partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context) partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids] msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
if 'Date' in message: if message.get('Date'):
try: try:
date_hdr = decode(message.get('Date')) date_hdr = decode(message.get('Date'))
parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True) parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
@ -795,12 +791,12 @@ class mail_thread(osv.AbstractModel):
stored_date = datetime.datetime.now() stored_date = datetime.datetime.now()
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
if 'In-Reply-To' in message: if message.get('In-Reply-To'):
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))]) parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
if parent_ids: if parent_ids:
msg_dict['parent_id'] = parent_ids[0] msg_dict['parent_id'] = parent_ids[0]
if 'References' in message and 'parent_id' not in msg_dict: if message.get('References') and 'parent_id' not in msg_dict:
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
[x.strip() for x in decode(message['References']).split()])]) [x.strip() for x in decode(message['References']).split()])])
if parent_ids: if parent_ids:
@ -821,77 +817,151 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.") "now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context) self.message_post(cr, uid, [id], message, context=context)
def message_create_partners_from_emails(self, cr, uid, emails, context=None): def _message_add_suggested_recipient(self, cr, uid, result, obj, partner=None, email=None, reason='', context=None):
""" Called by message_get_suggested_recipients, to add a suggested
recipient in the result dictionary. The form is :
partner_id, partner_name<partner_email> or partner_name, reason """
if email and not partner:
partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context)[0]
if partner_info.get('partner_id'):
partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info.get('partner_id')], context=context)[0]
if email and email in [val[1] for val in result[obj.id]]: # already existing email -> skip
return result
if partner and partner in obj.message_follower_ids: # recipient already in the followers -> skip
return result
if partner and partner in [val[0] for val in result[obj.id]]: # already existing partner ID -> skip
return result
if partner and partner.email: # complete profile: id, name <email>
result[obj.id].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
elif partner: # incomplete profile: id, name
result[obj.id].append((partner.id, '%s' % (partner.name), reason))
else: # unknown partner, we are probably managing an email address
result[obj.id].append((False, email, reason))
return result
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
""" Returns suggested recipients for ids. Those are a list of
tuple (partner_id, partner_name, reason), to be managed by Chatter. """
result = dict.fromkeys(ids, list())
if self._all_columns.get('user_id'):
for obj in self.browse(cr, SUPERUSER_ID, ids, context=context): # SUPERUSER because of a read on res.users that would crash otherwise
if not obj.user_id or not obj.user_id.partner_id:
continue
self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
return result
def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
""" Convert a list of emails into a list partner_ids and a list """ Convert a list of emails into a list partner_ids and a list
new_partner_ids. The return value is non conventional because new_partner_ids. The return value is non conventional because
it is meant to be used by the mail widget. it is meant to be used by the mail widget.
:return dict: partner_ids and new_partner_ids :return dict: partner_ids and new_partner_ids
""" """
partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message') mail_message_obj = self.pool.get('mail.message')
partner_obj = self.pool.get('res.partner')
partner_ids = [] result = list()
new_partner_ids = []
for email in emails: for email in emails:
partner_info = {'full_name': email, 'partner_id': False}
m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL) m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
name = m.group(2) or m.group(0) if not m:
email = m.group(3) continue
ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context) email_address = m.group(3)
ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email_address)], context=context)
if ids: if ids:
partner_ids.append(ids[0]) partner_info['partner_id'] = ids[0]
partner_id = ids[0] result.append(partner_info)
else:
partner_id = partner_obj.create(cr, uid, {
'name': name or email,
'email': email,
}, context=context)
new_partner_ids.append(partner_id)
# link mail with this from mail to the new partner id # link mail with this from mail to the new partner id
message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context) if link_mail and ids:
if message_ids: message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context) '|',
return { ('email_from', '=', email),
'partner_ids': partner_ids, ('email_from', 'ilike', '<%s>' % email),
'new_partner_ids': new_partner_ids, ('author_id', '=', False)
} ], context=context)
if message_ids:
mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': ids[0]}, context=context)
return result
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs): subtype=None, parent_id=False, attachments=None, context=None,
content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new """ Post a new message in an existing thread, returning the new
mail.message ID. Extra keyword arguments will be used as default mail.message ID.
column values for the new mail.message record.
Auto link messages for same id and object
:param int thread_id: thread ID to post into, or list with one ID; :param int thread_id: thread ID to post into, or list with one ID;
if False/0, mail.message model will also be set as False if False/0, mail.message model will also be set as False
:param str body: body of the message, usually raw HTML that will :param str body: body of the message, usually raw HTML that will
be sanitized be sanitized
:param str subject: optional subject :param str type: see mail_message.type field
:param str type: mail_message.type :param str content_subtype:: if plaintext: convert body into html
:param int parent_id: optional ID of parent message in this thread :param int parent_id: handle reply to a previous message by adding the
parent partners to the message in case of private discussion
:param tuple(str,str) attachments or list id: list of attachment tuples in the form :param tuple(str,str) attachments or list id: list of attachment tuples in the form
``(name,content)``, where content is NOT base64 encoded ``(name,content)``, where content is NOT base64 encoded
:return: ID of newly created mail.message
Extra keyword arguments will be used as default column values for the
new mail.message record. Special cases:
- attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter.
:return int: ID of newly created mail.message
""" """
if context is None: if context is None:
context = {} context = {}
if attachments is None: if attachments is None:
attachments = {} attachments = {}
assert (not thread_id) or isinstance(thread_id, (int, long)) or \
(isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id; should be 0, False, an ID or a list with one ID"
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id and thread_id[0]
mail_message = self.pool.get('mail.message') mail_message = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment')
assert (not thread_id) or \
isinstance(thread_id, (int, long)) or \
(isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), \
"Invalid thread_id; should be 0, False, an ID or a list with one ID"
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id[0]
# if we're processing a message directly coming from the gateway, the destination model was # if we're processing a message directly coming from the gateway, the destination model was
# set in the context. # set in the context.
model = False model = False
if thread_id: if thread_id:
model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
attachment_ids = kwargs.pop('attachment_ids', []) # 1: Handle content subtype: if plaintext, converto into HTML
if content_subtype == 'plaintext':
body = tools.plaintext2html(body)
# 2: Private message: add recipients (recipients and author of parent message)
# + legacy-code management (! we manage only 4 and 6 commands)
partner_ids = set()
kwargs_partner_ids = kwargs.pop('partner_ids', [])
for partner_id in kwargs_partner_ids:
if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2:
partner_ids.add(partner_id[1])
if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3:
partner_ids |= set(partner_id[2])
elif isinstance(partner_id, (int, long)):
partner_ids.add(partner_id)
else:
pass # we do not manage anything else
if parent_id and model == 'mail.thread':
parent_message = mail_message.browse(cr, uid, parent_id, context=context)
partner_ids |= set([partner.id for partner in parent_message.partner_ids])
if parent_message.author_id:
partner_ids.add(parent_message.author_id.id)
# 3. Attachments
# - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
attachment_ids = kwargs.pop('attachment_ids', []) or [] # because we could receive None (some old code sends None)
if attachment_ids:
filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
('res_model', '=', 'mail.compose.message'),
('res_id', '=', 0),
('create_uid', '=', uid),
('id', 'in', attachment_ids)], context=context)
if filtered_attachment_ids:
ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
attachment_ids = [(4, id) for id in attachment_ids]
# Handle attachments parameter, that is a dictionary of attachments
for name, content in attachments: for name, content in attachments:
if isinstance(content, unicode): if isinstance(content, unicode):
content = content.encode('utf-8') content = content.encode('utf-8')
@ -900,20 +970,25 @@ class mail_thread(osv.AbstractModel):
'datas': base64.b64encode(str(content)), 'datas': base64.b64encode(str(content)),
'datas_fname': name, 'datas_fname': name,
'description': name, 'description': name,
'res_model': context.get('thread_model') or self._name, 'res_model': model,
'res_id': thread_id, 'res_id': thread_id,
} }
attachment_ids.append((0, 0, data_attach)) attachment_ids.append((0, 0, data_attach))
# fetch subtype # 4: mail.message.subtype
subtype_id = False
if subtype: if subtype:
s_data = subtype.split('.') if '.' not in subtype:
if len(s_data) == 1: subtype = 'mail.%s' % subtype
s_data = ('mail', s_data[0]) ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s_data[0], s_data[1])
subtype_id = ref and ref[1] or False subtype_id = ref and ref[1] or False
else:
subtype_id = False # automatically subscribe recipients if asked to
if context.get('mail_post_autofollow') and thread_id and partner_ids:
partner_to_subscribe = partner_ids
if context.get('mail_post_autofollow_partner_ids'):
partner_to_subscribe = filter(lambda item: item in context.get('mail_post_autofollow_partner_ids'), partner_ids)
self.message_subscribe(cr, uid, [thread_id], list(partner_to_subscribe), context=context)
# _mail_flat_thread: automatically set free messages to the first posted message # _mail_flat_thread: automatically set free messages to the first posted message
if self._mail_flat_thread and not parent_id and thread_id: if self._mail_flat_thread and not parent_id and thread_id:
@ -941,86 +1016,34 @@ class mail_thread(osv.AbstractModel):
'parent_id': parent_id, 'parent_id': parent_id,
'attachment_ids': attachment_ids, 'attachment_ids': attachment_ids,
'subtype_id': subtype_id, 'subtype_id': subtype_id,
'partner_ids': [(4, pid) for pid in partner_ids],
}) })
# Avoid warnings about non-existing fields # Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc'): for x in ('from', 'to', 'cc'):
values.pop(x, None) values.pop(x, None)
return mail_message.create(cr, uid, values, context=context) # Create and auto subscribe the author
msg_id = mail_message.create(cr, uid, values, context=context)
message = mail_message.browse(cr, uid, msg_id, context=context)
if message.author_id and thread_id and type != 'notification':
self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
return msg_id
#------------------------------------------------------
# Compatibility methods: do not use
# TDE TODO: remove me in 8.0
#------------------------------------------------------
def message_create_partners_from_emails(self, cr, uid, emails, context=None):
return {'partner_ids': [], 'new_partner_ids': []}
def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False, def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
attachment_ids=None, content_subtype='plaintext', attachment_ids=None, content_subtype='plaintext',
context=None, **kwargs): context=None, **kwargs):
""" Wrapper on message_post, used for user input : return self.message_post(cr, uid, thread_id, body=body, parent_id=parent_id,
- mail gateway attachment_ids=attachment_ids, content_subtype=content_subtype,
- quick reply in Chatter (refer to mail.js), not context=context, **kwargs)
the mail.compose.message wizard
The purpose is to perform some pre- and post-processing:
- if body is plaintext: convert it into html
- if parent_id: handle reply to a previous message by adding the
parent partners to the message
- type and subtype: comment and mail.mt_comment by default
- attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter.
"""
mail_message_obj = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment')
# 1.A.1: add recipients of parent message (# TDE FIXME HACK: mail.thread -> private message)
partner_ids = set([])
if parent_id and self._name == 'mail.thread':
parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
if parent_message.author_id.id:
partner_ids.add((4, parent_message.author_id.id))
# 1.A.2: add specified recipients
param_partner_ids = set()
for item in kwargs.pop('partner_ids', []):
if isinstance(item, (list)):
param_partner_ids.add((item[0], item[1]))
elif isinstance(item, (int, long)):
param_partner_ids.add((4, item))
else:
param_partner_ids.add(item)
partner_ids |= param_partner_ids
# 1.A.3: add parameters recipients as follower
# TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
if param_partner_ids and self._name != 'mail.thread':
self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
# 1.B: handle body, message_type and message_subtype
if content_subtype == 'plaintext':
body = tools.plaintext2html(body)
msg_type = kwargs.pop('type', 'comment')
msg_subtype = kwargs.pop('subtype', 'mail.mt_comment')
# 2. Pre-processing: attachments
# HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
if attachment_ids:
# TDE FIXME (?): when posting a private message, we use mail.thread as a model
# However, attaching doc to mail.thread is not possible, mail.thread does not have any table
model = self._name
if model == 'mail.thread':
model = False
filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
('res_model', '=', 'mail.compose.message'),
('res_id', '=', 0),
('create_uid', '=', uid),
('id', 'in', attachment_ids)], context=context)
if filtered_attachment_ids:
if thread_id and model:
ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
else:
attachment_ids = []
attachment_ids = [(4, id) for id in attachment_ids]
# 3. Post message
return self.message_post(cr, uid, thread_id=thread_id, body=body,
type=msg_type, subtype=msg_subtype, parent_id=parent_id,
attachment_ids=attachment_ids, partner_ids=list(partner_ids), context=context, **kwargs)
#------------------------------------------------------ #------------------------------------------------------
# Followers API # Followers API
@ -1142,12 +1165,25 @@ class mail_thread(osv.AbstractModel):
# add followers coming from res.users relational fields that are tracked # 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)] 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)]: user_id_partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]
for partner_id in user_id_partner_ids:
new_followers.setdefault(partner_id, None) new_followers.setdefault(partner_id, None)
for pid, subtypes in new_followers.items(): for pid, subtypes in new_followers.items():
subtypes = list(subtypes) if subtypes is not None else None subtypes = list(subtypes) if subtypes is not None else None
self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context) self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
# find first email message, set it as unread for auto_subscribe fields for them to have a notification
if user_id_partner_ids:
msg_ids = self.pool.get('mail.message').search(cr, uid, [
('model', '=', self._name),
('res_id', '=', record.id),
('type', '=', 'email')], limit=1, context=context)
if not msg_ids and record.message_ids:
msg_ids = [record.message_ids[-1].id]
if msg_ids:
self.pool.get('mail.notification')._notify(cr, uid, msg_ids[0], partners_to_notify=user_id_partner_ids, context=context)
return True return True
#------------------------------------------------------ #------------------------------------------------------

View File

@ -6,7 +6,7 @@
<field name="tag">mail.wall</field> <field name="tag">mail.wall</field>
<field name="res_model">mail.message</field> <field name="res_model">mail.message</field>
<field name="context">{ <field name="context">{
'default_model': 'res.users', 'default_model': 'res.users',
'default_res_id': uid, 'default_res_id': uid,
}</field> }</field>
<field name="params" eval="&quot;{ <field name="params" eval="&quot;{
@ -34,8 +34,8 @@
<field name="tag">mail.wall</field> <field name="tag">mail.wall</field>
<field name="res_model">mail.message</field> <field name="res_model">mail.message</field>
<field name="context">{ <field name="context">{
'default_model': 'res.users', 'default_model': 'res.users',
'default_res_id': uid, 'default_res_id': uid,
'search_default_message_unread': True 'search_default_message_unread': True
}</field> }</field>
<field name="params" eval="&quot;{ <field name="params" eval="&quot;{
@ -44,7 +44,8 @@
], ],
'view_mailbox': True, 'view_mailbox': True,
'read_action': 'read', 'read_action': 'read',
'show_compose_message': False }&quot;"/> 'show_compose_message': False
}&quot;"/>
<field name="help" type="html"> <field name="help" type="html">
<p> <p>
<b>No private message.</b> <b>No private message.</b>
@ -59,8 +60,8 @@
<field name="tag">mail.wall</field> <field name="tag">mail.wall</field>
<field name="res_model">mail.message</field> <field name="res_model">mail.message</field>
<field name="context">{ <field name="context">{
'default_model': 'res.users', 'default_model': 'res.users',
'default_res_id': uid, 'default_res_id': uid,
'search_default_message_unread': True 'search_default_message_unread': True
}</field> }</field>
<field name="params" eval="&quot;{ <field name="params" eval="&quot;{
@ -69,7 +70,8 @@
], ],
'view_mailbox': True, 'view_mailbox': True,
'read_action': 'read', 'read_action': 'read',
'compose_as_todo': True }&quot;"/> 'compose_as_todo': True
}&quot;"/>
<field name="help" type="html"> <field name="help" type="html">
<p> <p>
<b>No todo.</b> <b>No todo.</b>
@ -84,7 +86,7 @@
<field name="name">Archives</field> <field name="name">Archives</field>
<field name="tag">mail.wall</field> <field name="tag">mail.wall</field>
<field name="context">{ <field name="context">{
'default_model': 'res.users', 'default_model': 'res.users',
'default_res_id': uid 'default_res_id': uid
}</field> }</field>
<field name="params" eval="&quot;{ <field name="params" eval="&quot;{
@ -94,7 +96,8 @@
('author_id.user_ids', 'in', [uid]), ('author_id.user_ids', 'in', [uid]),
], ],
'view_mailbox': True, 'view_mailbox': True,
'show_compose_message': False }&quot;"/> 'show_compose_message': False
}&quot;"/>
<field name="help" type="html"> <field name="help" type="html">
<p> <p>
No message found and no message sent yet. No message found and no message sent yet.

View File

@ -18,9 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################## ##############################################################################
from openerp.tools.translate import _
from openerp.osv import fields, osv from openerp.osv import fields, osv
class res_partner_mail(osv.Model): class res_partner_mail(osv.Model):
""" Update partner to add a field about notification preferences """ """ Update partner to add a field about notification preferences """
_name = "res.partner" _name = "res.partner"
@ -29,19 +30,28 @@ class res_partner_mail(osv.Model):
_columns = { _columns = {
'notification_email_send': fields.selection([ 'notification_email_send': fields.selection([
('all', 'All feeds'), ('none', 'Never'),
('comment', 'Comments and Emails'), ('email', 'Incoming Emails only'),
('email', 'Emails only'), ('comment', 'Incoming Emails and Discussions'),
('none', 'Never') ('all', 'All Messages (discussions, emails, followed system notifications)'),
], 'Receive Feeds by Email', required=True, ], 'Receive Messages by Email', required=True,
help="Choose in which case you want to receive an email when you "\ help="Policy to receive emails for new messages pushed to your personal Inbox:\n"
"receive new feeds."), "- Never: no emails are sent\n"
"- Incoming Emails only: for messages received by the system via email\n"
"- Incoming Emails and Discussions: for incoming emails along with internal discussions\n"
"- All Messages: for every notification you receive in your Inbox"),
} }
_defaults = { _defaults = {
'notification_email_send': lambda *args: 'comment' 'notification_email_send': lambda *args: 'comment'
} }
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(res_partner_mail, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for partner in self.browse(cr, uid, ids, context=context):
self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile'))
return recipients
def message_post(self, cr, uid, thread_id, **kwargs): def message_post(self, cr, uid, thread_id, **kwargs):
""" Override related to res.partner. In case of email message, set it as """ Override related to res.partner. In case of email message, set it as
private: private:

View File

@ -7,6 +7,9 @@
<field name="model">res.partner</field> <field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/> <field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='active']" position="after">
<field name='notification_email_send'/>
</xpath>
<xpath expr="//sheet" position="after"> <xpath expr="//sheet" position="after">
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>

View File

@ -113,13 +113,6 @@ class res_users(osv.Model):
thread_id = thread_id[0] thread_id = thread_id[0]
return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id
def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
various mailboxes, we do not have access to the current partner_id. """
partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
def message_post(self, cr, uid, thread_id, context=None, **kwargs): def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner. """ Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the This is done because when giving the context of Chatter on the
@ -139,6 +132,27 @@ class res_users(osv.Model):
self.pool.get('res.partner').message_subscribe(cr, uid, [partner_id], partner_ids, subtype_ids=subtype_ids, context=context) self.pool.get('res.partner').message_subscribe(cr, uid, [partner_id], partner_ids, subtype_ids=subtype_ids, context=context)
return True return True
def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
return self.pool.get('res.partner').message_get_partner_info_from_emails(cr, uid, emails, link_mail=link_mail, context=context)
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
partner_ids = []
for id in ids:
partner_ids.append(self.browse(cr, SUPERUSER_ID, id).partner_id.id)
return self.pool.get('res.partner').message_get_suggested_recipients(cr, uid, partner_ids, context=context)
#------------------------------------------------------
# Compatibility methods: do not use
# TDE TODO: remove me in 8.0
#------------------------------------------------------
def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
various mailboxes, we do not have access to the current partner_id. """
partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
def message_create_partners_from_emails(self, cr, uid, emails, context=None): def message_create_partners_from_emails(self, cr, uid, emails, context=None):
return self.pool.get('res.partner').message_create_partners_from_emails(cr, uid, emails, context=context) return self.pool.get('res.partner').message_create_partners_from_emails(cr, uid, emails, context=context)

View File

@ -54,6 +54,9 @@
min-height: 42px; min-height: 42px;
border: solid 1px rgba(0,0,0,0.03); border: solid 1px rgba(0,0,0,0.03);
} }
.openerp .oe_mail .oe_msg.oe_msg_nobody{
background: #F8F8F8;
}
.openerp .oe_mail .oe_msg .oe_msg_left{ .openerp .oe_mail .oe_msg .oe_msg_left{
position: absolute; position: absolute;
left:0; top: 0; bottom: 0; width: 40px; left:0; top: 0; bottom: 0; width: 40px;
@ -224,20 +227,30 @@
height: 24px; height: 24px;
width: 100%; width: 100%;
} }
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_sep_word{
margin-right: 8px;
margin-left: 8px;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact{ .openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact{
height: 24px; height: 24px;
width: 100%; width: 100%;
padding: 2px 4px; padding: 2px 4px;
color: #AAA;
cursor: text;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_record {
font-size: 13px;
font-style: bold;
text-align: center;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_inbox {
border: 1px solid #CCC; border: 1px solid #CCC;
-moz-border-radius: 3px; -moz-border-radius: 3px;
-webkit-border-radius: 3px; -webkit-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
background: white; background: white;
font-size: 14px; font-size: 14px;
color: #AAA;
font-style: italic; font-style: italic;
word-spacing: 3px;
cursor: text;
} }
/* d) I.E. tweaks for Message action icons */ /* d) I.E. tweaks for Message action icons */
@ -269,20 +282,20 @@
margin-top: 4px; margin-top: 4px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list{ .openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list {
display: block; display: block;
} }
.openerp .oe_mail .oe_msg_composer .oe_emails_from{ .openerp .oe_mail .oe_msg_composer .oe_recipients {
font-size: 12px; font-size: 12px;
margin-left: 20px; margin-left: 20px;
margin-bottom: 2px; margin-bottom: 2px;
} }
.openerp .oe_mail .oe_msg_composer .oe_emails_from label{ .openerp .oe_mail .oe_msg_composer .oe_recipients label{
vertical-align: middle; vertical-align: middle;
display: block; display: block;
line-height: 14px; line-height: 14px;
} }
.openerp .oe_mail .oe_msg_composer .oe_emails_from input{ .openerp .oe_mail .oe_msg_composer .oe_recipients input{
vertical-align: middle; vertical-align: middle;
} }
.openerp .oe_mail .oe_attachment{ .openerp .oe_mail .oe_attachment{
@ -439,7 +452,8 @@
line-height: 12px; line-height: 12px;
vertical-align: middle; vertical-align: middle;
} }
.openerp .oe_mail .oe_msg_footer button.oe_post{ .openerp .oe_mail .oe_msg_footer button.oe_post,
.openerp .oe_mail .oe_msg_footer button.oe_log{
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
@ -458,18 +472,20 @@
.openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{ .openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{
display:inline; display:inline;
} }
.openerp .oe_mail .oe_msg_footer button.oe_full{ .openerp .oe_mail .oe_msg_center button.oe_full{
width:24px; width: 24px;
overflow:hidden; height: 22px;
overflow: hidden;
float: right; float: right;
filter:none; filter: none;
} }
.openerp .oe_mail .oe_msg_footer button.oe_full .oe_e{ .openerp .oe_mail .oe_msg_center button.oe_full .oe_e{
position: relative; position: relative;
top: -4px; top: -9px;
margin-left: -9px; margin-left: -5px;
vertical-align: middle; vertical-align: top;
filter:none; filter: none;
height: 14px;
} }
.openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{ .openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{
background: transparent; background: transparent;

View File

@ -21,6 +21,19 @@ openerp.mail = function (session) {
mail.ChatterUtils = { mail.ChatterUtils = {
/** parse text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False */
parse_email: function (text) {
var result = text.match(/(.*)<(.*@.*)>/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
}
result = text.match(/(.*@.*)/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[1])];
}
return [text, false];
},
/* Get an image in /web/binary/image?... */ /* Get an image in /web/binary/image?... */
get_image: function (session, model, field, id, resize) { get_image: function (session, model, field, id, resize) {
var r = resize ? encodeURIComponent(resize) : ''; var r = resize ? encodeURIComponent(resize) : '';
@ -255,11 +268,15 @@ openerp.mail = function (session) {
this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
} }
if (this.author_id && this.author_id[1]) { if (this.author_id && this.author_id[1]) {
var email = this.author_id[1].match(/(.*)<(.*@.*)>/); var parsed_email = mail.ChatterUtils.parse_email(this.author_id[1]);
if (!email) { this.author_id.push(parsed_email[0], parsed_email[1]);
this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]); }
} else { if (this.partner_ids && this.partner_ids.length > 3) {
this.author_id.push(_.str.escapeHTML(email[0]), _.str.trim(email[1]), email[2]); this.extra_partners_nbr = this.partner_ids.length - 3;
this.extra_partners_str = ''
var extra_partners = this.partner_ids.slice(3);
for (var key in extra_partners) {
this.extra_partners_str += extra_partners[key][1];
} }
} }
}, },
@ -371,14 +388,24 @@ openerp.mail = function (session) {
* @param {Object} [context] context passed to the * @param {Object} [context] context passed to the
* mail.compose.message DataSetSearch. Please refer to this model * mail.compose.message DataSetSearch. Please refer to this model
* for more details about fields and default values. * for more details about fields and default values.
* @param {Object} recipients = [
{
'email_address': [str],
'partner_id': False/[int],
'name': [str],
'full_name': name<email_address>,
},
{ ... },
]
*/ */
init: function (parent, datasets, options) { init: function (parent, datasets, options) {
this._super(parent, datasets, options); this._super(parent, datasets, options);
this.show_compact_message = false; this.show_compact_message = false;
this.show_delete_attachment = true; this.show_delete_attachment = true;
this.emails_from = []; this.is_log = false;
this.partners_from = []; this.recipients = [];
this.recipient_ids = [];
}, },
start: function () { start: function () {
@ -479,15 +506,13 @@ openerp.mail = function (session) {
bind_events: function () { bind_events: function () {
var self = this; var self = this;
this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer);
this.$('.oe_compact').on('click', _.bind( this.on_compose_expandable, this)); this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer);
this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer);
// set the function called when attachments are added this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this));
this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this) ); this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this));
this.$('.oe_post').on('click', self.on_message_post);
this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this) ); this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment'));
this.$('.oe_post').on('click', _.bind( this.on_message_post, this) );
this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment') );
/* stack for don't close the compose form if the user click on a button */ /* stack for don't close the compose form if the user click on a button */
this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this)); this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this)); this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this));
@ -497,13 +522,12 @@ openerp.mail = function (session) {
this.$('textarea').autosize(); this.$('textarea').autosize();
// auto close // auto close
this.$('textarea').on('blur', _.bind( this.on_compose_expandable, this)); this.$('textarea').on('blur', self.on_toggle_quick_composer);
// event: delete child attachments off the oe_msg_attachment_list box // event: delete child attachments off the oe_msg_attachment_list box
this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete); this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from); this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient);
this.$(".oe_partners_from").on('change', 'input', this.on_checked_partner_from);
}, },
on_compose_fullmail: function (default_composition_mode) { on_compose_fullmail: function (default_composition_mode) {
@ -511,16 +535,26 @@ openerp.mail = function (session) {
if(!this.do_check_attachment_upload()) { if(!this.do_check_attachment_upload()) {
return false; return false;
} }
var recipient_done = $.Deferred();
// create list of new partners if (this.is_log) {
this.check_recipient_partners().done(function (partner_ids) { recipient_done.resolve([]);
}
else {
recipient_done = this.check_recipient_partners();
}
$.when(recipient_done).done(function (partner_ids) {
var context = { var context = {
'default_composition_mode': default_composition_mode, 'default_composition_mode': default_composition_mode,
'default_parent_id': self.id, 'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''), 'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),
'default_partner_ids': partner_ids, 'default_partner_ids': partner_ids,
'mail_post_autofollow': true,
'mail_post_autofollow_partner_ids': partner_ids,
}; };
if (self.is_log) {
_.extend(context, {'mail_compose_log': true});
}
if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) { if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
context.default_model = self.context.default_model; context.default_model = self.context.default_model;
context.default_res_id = self.context.default_res_id; context.default_res_id = self.context.default_res_id;
@ -573,68 +607,122 @@ openerp.mail = function (session) {
check_recipient_partners: function () { check_recipient_partners: function () {
var self = this; var self = this;
var partners_from = []; var check_done = $.Deferred();
var emails = []; var recipients = _.filter(this.recipients, function (recipient) { return recipient.checked });
_.each(this.emails_from, function (email_from) { var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id) });
if (email_from[1] && !_.find(emails, function (email) {return email == email_from[0][4];})) { var names_to_find = _.pluck(recipients_to_find, 'full_name');
emails.push(email_from[0][1]); var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address) });
} var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address }), 'partner_id');
}); var names_to_remove = [];
var deferred_check = $.Deferred(); var recipient_ids_to_remove = [];
if (emails.length == 0) {
return deferred_check.resolve(partners_from); // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
var find_done = $.Deferred();
if (names_to_find.length > 0) {
find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [names_to_find]);
} }
self.parent_thread.ds_thread._model.call('message_create_partners_from_emails', [emails]).then(function (partners) { else {
partners_from = _.clone(partners.partner_ids); find_done.resolve([]);
var deferreds = []; }
_.each(partners.new_partner_ids, function (id) {
// for unknown names + incomplete partners -> open popup - cancel = remove from recipients
$.when(find_done).pipe(function (result) {
var emails_deferred = [];
var recipient_popups = result.concat(recipients_to_check);
_.each(recipient_popups, function (partner_info) {
var deferred = $.Deferred() var deferred = $.Deferred()
deferreds.push(deferred); emails_deferred.push(deferred);
var pop = new session.web.form.FormOpenPopup(this);
var partner_name = partner_info.full_name;
var partner_id = partner_info.partner_id;
var parsed_email = mail.ChatterUtils.parse_email(partner_name);
var pop = new session.web.form.FormOpenPopup(this);
pop.show_element( pop.show_element(
'res.partner', 'res.partner',
id, partner_id,
{ { 'force_email': true,
'force_email': true,
'ref': "compound_context", 'ref': "compound_context",
}, 'default_name': parsed_email[0],
{ 'default_email': parsed_email[1],
}, {
title: _t("Please complete partner's informations"), title: _t("Please complete partner's informations"),
} }
); );
pop.on('closed', self, function () { pop.on('closed', self, function () {
deferred.resolve(); deferred.resolve();
}); });
partners_from.push(id); pop.view_form.on('on_button_cancel', self, function () {
names_to_remove.push(partner_name);
if (partner_id) {
recipient_ids_to_remove.push(partner_id);
}
});
}); });
$.when.apply( $, deferreds ).then(function () {
deferred_check.resolve(partners_from); $.when.apply($, emails_deferred).then(function () {
var new_names_to_find = _.difference(names_to_find, names_to_remove);
find_done = $.Deferred();
if (new_names_to_find.length > 0) {
find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [new_names_to_find, true]);
}
else {
find_done.resolve([]);
}
$.when(find_done).pipe(function (result) {
var recipient_popups = result.concat(recipients_to_check);
_.each(recipient_popups, function (partner_info) {
if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
recipient_ids.push(partner_info.partner_id);
}
});
}).pipe(function () {
check_done.resolve(recipient_ids);
});
}); });
}); });
return deferred_check;
return check_done;
}, },
on_message_post: function (event) { on_message_post: function (event) {
var self = this; var self = this;
if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) { if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
// create list of new partners if (this.is_log) {
this.check_recipient_partners().done(function (partner_ids) { this.do_send_message_post([], this.is_log);
self.do_send_message_post(partner_ids); }
}); else {
this.check_recipient_partners().done(function (partner_ids) {
self.do_send_message_post(partner_ids, self.is_log);
});
}
} }
}, },
/*do post a message and fetch the message*/ /* do post a message and fetch the message */
do_send_message_post: function (partner_ids) { do_send_message_post: function (partner_ids, log) {
var self = this; var self = this;
this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], { var values = {
'body': this.$('textarea').val(), 'body': this.$('textarea').val(),
'subject': false, 'subject': false,
'parent_id': this.context.default_parent_id, 'parent_id': this.context.default_parent_id,
'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}), 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
'partner_ids': partner_ids, 'partner_ids': partner_ids,
'context': this.parent_thread.context, 'context': _.extend(this.parent_thread.context, {
}).done(function (message_id) { 'mail_post_autofollow': true,
'mail_post_autofollow_partner_ids': partner_ids,
}),
'type': 'comment',
'content_subtype': 'plaintext',
};
if (log) {
values['subtype'] = false;
}
else {
values['subtype'] = 'mail.mt_comment';
}
this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) {
var thread = self.parent_thread; var thread = self.parent_thread;
var root = thread == self.options.root_thread; var root = thread == self.options.root_thread;
if (self.options.display_indented_thread < self.thread_level && thread.parent_message) { if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
@ -650,18 +738,55 @@ openerp.mail = function (session) {
}); });
}, },
/* convert the compact mode into the compose message /* Quick composer: toggle minimal / expanded mode
*/ * - toggle minimal (one-liner) / expanded (textarea, buttons) mode
on_compose_expandable: function (event) { * - when going into expanded mode:
this.get_emails_from(); * - call `message_get_suggested_recipients` to have a list of partners to add
if ((!this.stay_open || (event && event.type == 'click')) && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/) && !this.attachment_ids.length)) { * - compute email_from list (list of unknown email_from to propose to create partners)
this.show_composer = !this.show_composer || this.stay_open; */
this.reinit(); on_toggle_quick_composer: function (event) {
var self = this;
var $input = $(event.target);
this.compute_emails_from();
var email_addresses = _.pluck(this.recipients, 'email_address');
var suggested_partners = $.Deferred();
// if clicked: call for suggested recipients
if (event.type == 'click') {
this.is_log = $input.hasClass('oe_compose_log');
suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id]]).done(function (additional_recipients) {
var thread_recipients = additional_recipients[self.context.default_res_id];
_.each(thread_recipients, function (recipient) {
var parsed_email = mail.ChatterUtils.parse_email(recipient[1]);
if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
self.recipients.push({
'checked': true,
'partner_id': recipient[0],
'full_name': recipient[1],
'name': parsed_email[0],
'email_address': parsed_email[1],
'reason': recipient[2],
})
}
});
});
} }
if (!this.stay_open && this.show_composer && (!event || event.type != 'blur')) { else {
this.$('textarea:not(.oe_compact):first').focus(); suggested_partners.resolve({});
} }
return true;
// when call for suggested partners finished: re-render the widget
$.when(suggested_partners).pipe(function (additional_recipients) {
if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) {
self.show_composer = !self.show_composer || self.stay_open;
self.reinit();
}
if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) {
self.$('textarea:not(.oe_compact):first').focus();
}
});
return suggested_partners;
}, },
do_hide_compact: function () { do_hide_compact: function () {
@ -678,7 +803,10 @@ openerp.mail = function (session) {
} }
}, },
get_emails_from: function () { /** Compute the list of unknown email_from the the given thread
* TDE FIXME: seems odd to delegate to the composer
* TDE TODO: please de-obfuscate and comment your code */
compute_emails_from: function () {
var self = this; var self = this;
var messages = []; var messages = [];
@ -691,22 +819,28 @@ openerp.mail = function (session) {
// get all wall messages if is not a mail.Wall // get all wall messages if is not a mail.Wall
_.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());}); _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
} }
_.each(messages, function (thread) { _.each(messages, function (thread) {
if (thread.author_id && !thread.author_id[0] && if (thread.author_id && !thread.author_id[0] &&
!_.find(self.emails_from, function (from) {return from[0][4] == thread.author_id[4];})) { !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];})) {
self.emails_from.push([thread.author_id, true]); self.recipients.push({ 'full_name': thread.author_id[1],
'name': thread.author_id[2],
'email_address': thread.author_id[3],
'partner_id': false,
'checked': true,
'reason': 'Incoming email author'
});
} }
}); });
return self.emails_from; return self.recipients;
}, },
on_checked_email_from: function (event) { on_checked_recipient: function (event) {
var $input = $(event.target); var $input = $(event.target);
var email = $input.attr("data"); var email = $input.attr("data");
_.each(this.emails_from, function (email_from) { _.each(this.recipients, function (recipient) {
if (email_from[0][4] == email) { if (recipient.email_address == email) {
email_from[1] = $input.is(":checked"); recipient.checked = $input.is(":checked");
} }
}); });
}, },
@ -1236,12 +1370,12 @@ openerp.mail = function (session) {
/** /**
*If compose_message doesn't exist, instantiate the compose message. *If compose_message doesn't exist, instantiate the compose message.
* Call the on_compose_expandable method to allow the user to write his message. * Call the on_toggle_quick_composer method to allow the user to write his message.
* (Is call when a user click on "Reply" button) * (Is call when a user click on "Reply" button)
*/ */
on_compose_message: function (event) { on_compose_message: function (event) {
this.instantiate_compose_message(); this.instantiate_compose_message();
this.compose_message.on_compose_expandable(event); this.compose_message.on_toggle_quick_composer(event);
return false; return false;
}, },
@ -1810,7 +1944,7 @@ openerp.mail = function (session) {
}; };
session.client.action_manager.do_action(action); session.client.action_manager.do_action(action);
}); });
this.$(".oe_write_onwall").click(function(){ self.root.thread.on_compose_message(); }); this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); });
} }
}); });

View File

@ -106,9 +106,12 @@ openerp_mail_followers = function(session, mail) {
on_remove_follower: function (event) { on_remove_follower: function (event) {
var partner_id = $(event.target).data('id'); var partner_id = $(event.target).data('id');
var context = new session.web.CompoundContext(this.build_context(), {}); var name = $(event.target).parent().find("a").html();
return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context]) if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) {
.then(this.proxy('read_value')); var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
.then(this.proxy('read_value'));
}
}, },
read_value: function () { read_value: function () {
@ -248,12 +251,15 @@ openerp_mail_followers = function(session, mail) {
}, },
do_unfollow: function () { do_unfollow: function () {
_(this.$('.oe_msg_subtype_check')).each(function (record) { if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) {
$(record).attr('checked',false); _(this.$('.oe_msg_subtype_check')).each(function (record) {
}); $(record).attr('checked',false);
var context = new session.web.CompoundContext(this.build_context(), {}); });
return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context]) var context = new session.web.CompoundContext(this.build_context(), {});
.then(this.proxy('read_value')); return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
.then(this.proxy('read_value'));
}
return false;
}, },
do_update_subscription: function (event) { do_update_subscription: function (event) {
@ -267,7 +273,9 @@ openerp_mail_followers = function(session, mail) {
}); });
if (!checklist.length) { if (!checklist.length) {
this.do_unfollow(); if (!this.do_unfollow()) {
$(event.target).attr("checked", "checked");
}
} else { } else {
var context = new session.web.CompoundContext(this.build_context(), {}); var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], checklist, context]) return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], checklist, context])

View File

@ -26,24 +26,35 @@
</div> </div>
<div class="oe_msg_center"> <div class="oe_msg_center">
<div class="oe_msg_content"> <div class="oe_msg_content">
<button class="oe_full" title="Open the full mail composer"><span class='oe_e'>&amp;ograve</span></button>
<t t-call="mail.thread.list_recipients"/> <t t-call="mail.thread.list_recipients"/>
<textarea class="field_text"></textarea> <textarea class="field_text"></textarea>
</div> </div>
<div class="oe_msg_footer"> <div class="oe_msg_footer">
<div class="oe_msg_attachment_list"></div> <div class="oe_msg_attachment_list"></div>
<button class="oe_post">Post</button> <button class="oe_post">
<t t-if="!widget.is_log">Send</t>
<t t-if="widget.is_log">Log a note</t>
</button>
<t t-call="mail.compose_message.add_attachment"/> <t t-call="mail.compose_message.add_attachment"/>
<!--<a class="oe_cancel oe_e">X</a>-->
<button class="oe_full" title="Open the full mail composer"><span class='oe_e'>&amp;ograve</span></button>
</div> </div>
</div> </div>
</div> </div>
<div t-if="widget.show_compact_message and !widget.show_composer and !widget.options.readonly" t-attf-class="oe_msg oe_msg_composer_compact #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}"> <div t-if="widget.show_compact_message and !widget.show_composer and !widget.options.readonly" t-attf-class="oe_msg oe_msg_composer_compact #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<div class="field_text oe_compact"> <t t-if="widget.options.view_mailbox">
<t t-if="widget.options.compose_placeholder" t-raw="widget.options.compose_placeholder"/> <div class="field_text oe_compact oe_compact_inbox">
<t t-if="!widget.options.compose_placeholder and !widget.options.view_mailbox">Write to the followers of this document...</t> <t t-if="widget.options.compose_placeholder" t-raw="widget.options.compose_placeholder"/>
<t t-if="!widget.options.compose_placeholder and widget.options.view_mailbox">Share with my followers...</t> <t t-if="!widget.options.compose_placeholder and widget.options.view_mailbox">Share with my followers...</t>
</div> </div>
</t>
<t t-if="!widget.options.view_mailbox">
<div class="field_text oe_compact oe_compact_record">
<a class="oe_compose_post" t-if="widget.options.compose_placeholder"><t t-raw="widget.options.compose_placeholder"/></a>
<a class="oe_compose_post" t-if="!widget.options.compose_placeholder and !widget.options.view_mailbox">Send a message</a>
<span class="oe_grey oe_sep_word">or</span>
<a class="oe_compose_log">Log a note</a>
</div>
</t>
</div> </div>
<span t-if="!(widget.show_compact_message and !widget.show_composer) and !widget.show_composer" class="oe_placeholder_compose"></span> <span t-if="!(widget.show_compact_message and !widget.show_composer) and !widget.show_composer" class="oe_placeholder_compose"></span>
</t> </t>
@ -57,10 +68,10 @@
<span class="oe_add"> <span class="oe_add">
<!-- uploader of file --> <!-- uploader of file -->
<button class="oe_attach"><span class="oe_e">'</span></button> <button class="oe_attach"><span class="oe_e">'</span></button>
<span class='oe_attach_label'>File</span> <span class='oe_attach_label'>Attach a File</span>
<t t-call="HiddenInputFile"> <t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/> <t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action">/web/binary/upload_attachment</t> <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" value="mail.compose.message"/> <input type="hidden" name="model" value="mail.compose.message"/>
<input type="hidden" name="id" value="0"/> <input type="hidden" name="id" value="0"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/> <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
@ -106,7 +117,7 @@
template to the recipients list template to the recipients list
--> -->
<t t-name="mail.thread.list_recipients"> <t t-name="mail.thread.list_recipients">
<div class="oe_mail_list_recipients"> <div class="oe_mail_list_recipients" t-if="!widget.is_log">
To: To:
<t t-if="!widget.is_private"> <t t-if="!widget.is_private">
<span class="oe_all_follower"> <span class="oe_all_follower">
@ -129,14 +140,17 @@
<a class="oe_more_hidden">&lt;&lt;&lt;</a> <a class="oe_more_hidden">&lt;&lt;&lt;</a>
</t> </t>
</div> </div>
<div class="oe_emails_from"> <div class="oe_recipients" t-if="!widget.is_log">
<t t-foreach='widget.emails_from' t-as='email_from'> <t t-foreach='widget.recipients' t-as='recipient'>
<label title="Add them into recipients and followers"> <label t-attf-title="Add as recipient and follower (reason: #{recipient.reason})">
<input type="checkbox" t-att-checked="email_from[1] ? 'checked' : undefind" t-att-data="email_from[0][4]"/> <input type="checkbox" t-att-checked="recipient.checked ? 'checked' : undefined" t-att-data="recipient.email_address"/>
<t t-raw="email_from[0][2]"/> <t t-raw="recipient.name"/> (<t t-raw="recipient.email_address"/>)
</label> </label>
</t> </t>
</div> </div>
<div t-if="widget.is_log">
<span>Attach a note that will not be sent to the followers</span>
</div>
</t> </t>
<!-- <!--
@ -212,7 +226,7 @@
<!-- default layout --> <!-- default layout -->
<t t-name="mail.thread.message"> <t t-name="mail.thread.message">
<div t-attf-class="oe_msg #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} oe_msg_#{widget.type}"> <div t-attf-class="oe_msg #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} #{widget.partner_ids.length == 0 ? 'oe_msg_nobody' : ''} oe_msg_#{widget.type}">
<div class='oe_msg_left'> <div class='oe_msg_left'>
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]"> <a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]">
@ -245,6 +259,22 @@
</t> </t>
<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> <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 t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-raw="widget.author_id[2]"/></span>
<t t-if="widget.partner_ids.length == 0">
logged a note
</t>
<t t-if="widget.partner_ids.length > 0">
to
</t>
<t t-foreach="widget.partner_ids.slice(0, 3)" t-as="partner">
<span t-attf-class="oe_partner_follower">
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-raw="partner[1]"/></a>
<t t-if="!widget.options.show_link" t-raw="partner[1]"/>
</span>
<t t-if="!partner_last">,</t>
</t>
<t t-if="widget.partner_ids.length > 3">
<span t-att-title="widget.extra_partners_str">and <t t-raw="widget.extra_partners_nbr"/> more</span>
</t>
<span class='oe_subtle'></span> <span class='oe_subtle'></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-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> <span t-if="!widget.options.readonly" class='oe_subtle'></span>

View File

@ -119,18 +119,18 @@ class test_mail(TestMailBase):
# Previously-created group can be emailed now - it should have an implicit alias group+frogs@... # Previously-created group can be emailed now - it should have an implicit alias group+frogs@...
frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
group_messages = frog_group.message_ids group_messages = frog_group.message_ids
self.assertTrue(len(group_messages) == 2, 'New group should only have the original message + creation log') self.assertTrue(len(group_messages) == 1, 'New group should only have the original message')
mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='') mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='')
self.mail_thread.message_process(cr, uid, None, mail_frog_news) self.mail_thread.message_process(cr, uid, None, mail_frog_news)
frog_group.refresh() frog_group.refresh()
self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now') self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now')
# Even with a wrong destination, a reply should end up in the correct thread # Even with a wrong destination, a reply should end up in the correct thread
mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news', mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id) extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
self.mail_thread.message_process(cr, uid, None, mail_reply) self.mail_thread.message_process(cr, uid, None, mail_reply)
frog_group.refresh() frog_group.refresh()
self.assertTrue(len(frog_group.message_ids) == 4, 'Group should contain 4 messages now') self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
# No model passed and no matching alias must raise # No model passed and no matching alias must raise
mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='') mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='')
@ -154,7 +154,7 @@ class test_mail(TestMailBase):
new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0]) new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
# Test: author_id set, not email_from # Test: author_id set, not email_from
self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found') self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found')
self.assertFalse(new_mail.email_from, 'message process should not set the email_from when an author is found') self.assertEqual(new_mail.email_from, user_raoul.email, 'message process wrong email_from')
# Do: post a new message, with a unknown partner # Do: post a new message, with a unknown partner
test_msg_id = '<deadcafe.1337-3@smtp.agrolait.com>' test_msg_id = '<deadcafe.1337-3@smtp.agrolait.com>'
@ -391,7 +391,8 @@ class test_mail(TestMailBase):
# 1. Post a new email comment on Pigs # 1. Post a new email comment on Pigs
self._init_mock_build_email() self._init_mock_build_email()
msg2_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment', msg2_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment',
partner_ids=[(6, 0, [p_d_id])], parent_id=msg1_id, attachments=_attachments) partner_ids=[p_d_id], parent_id=msg1_id, attachments=_attachments,
context={'mail_post_autofollow': True})
message2 = self.mail_message.browse(cr, uid, msg2_id) message2 = self.mail_message.browse(cr, uid, msg2_id)
sent_emails = self._build_email_kwargs_list sent_emails = self._build_email_kwargs_list
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg2_id)]), 'mail.mail notifications should have been auto-deleted!') self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg2_id)]), 'mail.mail notifications should have been auto-deleted!')
@ -482,7 +483,7 @@ class test_mail(TestMailBase):
self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id') self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
# 2. Post the comment, get created message # 2. Post the comment, get created message
mail_compose.send_mail(cr, uid, [compose_id]) mail_compose.send_mail(cr, uid, [compose_id], {'mail_post_autofollow': True})
group_pigs.refresh() group_pigs.refresh()
message = group_pigs.message_ids[0] message = group_pigs.message_ids[0]
# Test: mail.message: subject, body inside pre # Test: mail.message: subject, body inside pre

View File

@ -311,8 +311,8 @@ class test_mail_access_rights(TestMailBase):
user_bert_id, user_raoul_id = self.user_bert_id, self.user_raoul_id user_bert_id, user_raoul_id = self.user_bert_id, self.user_raoul_id
# Prepare groups: Pigs (employee), Jobs (public) # Prepare groups: Pigs (employee), Jobs (public)
pigs_msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message', partner_ids=[(4, self.partner_admin_id)]) pigs_msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message', partner_ids=[self.partner_admin_id])
jobs_msg_id = self.mail_group.message_post(cr, uid, self.group_jobs_id, body='Message', partner_ids=[(4, self.partner_admin_id)]) jobs_msg_id = self.mail_group.message_post(cr, uid, self.group_jobs_id, body='Message', partner_ids=[self.partner_admin_id])
# ---------------------------------------- # ----------------------------------------
# CASE1: Bert, without groups # CASE1: Bert, without groups

View File

@ -66,7 +66,7 @@ class invite_wizard(osv.osv_memory):
signature = user_id and user_id["signature"] or '' signature = user_id and user_id["signature"] or ''
if signature: if signature:
wizard.message = tools.append_content_to_html(wizard.message, signature, plaintext=True, container_tag='div') wizard.message = tools.append_content_to_html(wizard.message, signature, plaintext=True, container_tag='div')
# send mail to new followers # FIXME 8.0: use notification_email_send, send a wall message and let mail handle email notification + message box
for follower_id in new_follower_ids: for follower_id in new_follower_ids:
mail_mail = self.pool.get('mail.mail') mail_mail = self.pool.get('mail.mail')
# the invite wizard should create a private message not related to any object -> no model, no res_id # the invite wizard should create a private message not related to any object -> no model, no res_id

View File

@ -191,6 +191,7 @@ class mail_compose_message(osv.TransientModel):
if context is None: if context is None:
context = {} context = {}
active_ids = context.get('active_ids') active_ids = context.get('active_ids')
is_log = context.get('mail_compose_log', False)
for wizard in self.browse(cr, uid, ids, context=context): for wizard in self.browse(cr, uid, ids, context=context):
mass_mail_mode = wizard.composition_mode == 'mass_mail' mass_mail_mode = wizard.composition_mode == 'mass_mail'
@ -204,26 +205,22 @@ class mail_compose_message(osv.TransientModel):
'subject': wizard.subject, 'subject': wizard.subject,
'body': wizard.body, 'body': wizard.body,
'parent_id': wizard.parent_id and wizard.parent_id.id, 'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [(4, partner.id) for partner in wizard.partner_ids], 'partner_ids': [partner.id for partner in wizard.partner_ids],
'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids], 'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],
} }
# mass mailing: render and override default values # mass mailing: render and override default values
if mass_mail_mode and wizard.model: if mass_mail_mode and wizard.model:
email_dict = self.render_message(cr, uid, wizard, res_id, context=context) email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
new_partner_ids = email_dict.pop('partner_ids', []) new_partner_ids = email_dict.pop('partner_ids', [])
post_values['partner_ids'] += [(4, partner_id) for partner_id in new_partner_ids] post_values['partner_ids'] += new_partner_ids
new_attachments = email_dict.pop('attachments', []) new_attachments = email_dict.pop('attachments', [])
post_values['attachments'] += new_attachments post_values['attachments'] += new_attachments
post_values.update(email_dict) post_values.update(email_dict)
# automatically subscribe recipients if asked to
if context.get('mail_post_autofollow') and wizard.model and post_values.get('partner_ids'):
active_model_pool.message_subscribe(cr, uid, [res_id], [item[1] for item in post_values.get('partner_ids')], context=context)
# post the message # post the message
active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype='mt_comment', context=context, **post_values) subtype = 'mail.mt_comment'
if is_log:
# post process: update attachments, because id is not necessarily known when adding attachments in Chatter subtype = False
# self.pool.get('ir.attachment').write(cr, uid, [attach.id for attach in wizard.attachment_ids], { active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **post_values)
# 'res_id': wizard.id, 'res_model': wizard.model or False}, context=context)
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}
@ -258,7 +255,7 @@ class mail_compose_message(osv.TransientModel):
result = eval(exp, { result = eval(exp, {
'user': self.pool.get('res.users').browse(cr, uid, uid, context=context), 'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
'object': self.pool.get(model).browse(cr, uid, res_id, context=context), 'object': self.pool.get(model).browse(cr, uid, res_id, context=context),
'context': dict(context), # copy context to prevent side-effects of eval 'context': dict(context), # copy context to prevent side-effects of eval
}) })
return result and tools.ustr(result) or '' return result and tools.ustr(result) or ''
return template and EXPRESSION_PATTERN.sub(merge, template) return template and EXPRESSION_PATTERN.sub(merge, template)

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