diff --git a/addons/account/account.py b/addons/account/account.py index a14c44405b9..acf27c42392 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -3521,7 +3521,7 @@ class account_bank_accounts_wizard(osv.osv_memory): _columns = { 'acc_name': fields.char('Account Name.', size=64, required=True), - 'bank_account_id': fields.many2one('wizard.multi.charts.accounts', 'Bank Account', required=True), + 'bank_account_id': fields.many2one('wizard.multi.charts.accounts', 'Bank Account', required=True, ondelete='cascade'), 'currency_id': fields.many2one('res.currency', 'Secondary Currency', help="Forces all moves for this account to have this secondary currency."), 'account_type': fields.selection([('cash','Cash'), ('check','Check'), ('bank','Bank')], 'Account Type', size=32), } diff --git a/addons/account/account_bank.py b/addons/account/account_bank.py index f2bc9a5c9a1..44411cd637a 100644 --- a/addons/account/account_bank.py +++ b/addons/account/account_bank.py @@ -41,7 +41,7 @@ class bank(osv.osv): def _prepare_name(self, bank): "Return the name to use when creating a bank journal" - return (bank.bank_name or '') + ' ' + bank.acc_number + return (bank.bank_name or '') + ' ' + (bank.acc_number or '') def _prepare_name_get(self, cr, uid, bank_dicts, context=None): """Add ability to have %(currency_name)s in the format_layout of res.partner.bank.type""" diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index 6ca7c21a6e4..6cca8e0ce46 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -256,7 +256,7 @@ - + @@ -393,7 +393,7 @@ - diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 5e072c3d258..3ce60902d94 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -347,6 +347,15 @@ res_model="account.move.line" src_model="account.account"/> + + + account.journal.tree @@ -841,6 +850,16 @@ res_model="account.move.line" src_model="account.tax.code"/> + + + + account.tax.tree diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml index 0406efd4526..949d03324f3 100644 --- a/addons/account/data/account_data.xml +++ b/addons/account/data/account_data.xml @@ -155,11 +155,13 @@ Validated account.invoice + Invoice validated Paid account.invoice + Invoice paid diff --git a/addons/account/project/project_view.xml b/addons/account/project/project_view.xml index 1a596a4a7a9..8590ee286d4 100644 --- a/addons/account/project/project_view.xml +++ b/addons/account/project/project_view.xml @@ -12,9 +12,9 @@ - + - + @@ -30,15 +30,18 @@ - - + + + - - - + + + + + diff --git a/addons/account/report/account_invoice_report_view.xml b/addons/account/report/account_invoice_report_view.xml index ed3649c300a..dad70f695dc 100644 --- a/addons/account/report/account_invoice_report_view.xml +++ b/addons/account/report/account_invoice_report_view.xml @@ -58,6 +58,7 @@ + diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index f72e81fbc08..21c1ee3d846 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -35,10 +35,7 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header): 'lines': self.lines, 'sum_debit_partner': self._sum_debit_partner, 'sum_credit_partner': self._sum_credit_partner, -# 'sum_debit': self._sum_debit, -# 'sum_credit': self._sum_credit, 'get_currency': self._get_currency, - 'comma_me': self.comma_me, 'get_start_period': self.get_start_period, 'get_end_period': self.get_end_period, 'get_account': self._get_account, @@ -78,11 +75,6 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header): move_state = ['draft','posted'] if self.target_move == 'posted': move_state = ['posted'] - - if (data['model'] == 'res.partner'): - ## Si on imprime depuis les partenaires - if ids: - PARTNER_REQUEST = "AND line.partner_id IN %s",(tuple(ids),) if self.result_selection == 'supplier': self.ACCOUNT_TYPE = ['payable'] elif self.result_selection == 'customer': @@ -98,7 +90,11 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header): 'WHERE a.type IN %s' \ "AND a.active", (tuple(self.ACCOUNT_TYPE), )) self.account_ids = [a for (a,) in self.cr.fetchall()] - partner_to_use = [] + params = [tuple(move_state), tuple(self.account_ids)] + #if we print from the partners, add a clause on active_ids + if (data['model'] == 'res.partner') and ids: + PARTNER_REQUEST = "AND l.partner_id IN %s" + params += [tuple(ids)] self.cr.execute( "SELECT DISTINCT l.partner_id " \ "FROM account_move_line AS l, account_account AS account, " \ @@ -110,30 +106,10 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header): # "AND " + self.query +" " \ "AND l.account_id IN %s " \ " " + PARTNER_REQUEST + " " \ - "AND account.active ", - (tuple(move_state), tuple(self.account_ids),)) - - res = self.cr.dictfetchall() - for res_line in res: - partner_to_use.append(res_line['partner_id']) - new_ids = partner_to_use - self.partner_ids = new_ids - objects = obj_partner.browse(self.cr, self.uid, new_ids) - return super(third_party_ledger, self).set_context(objects, data, new_ids, report_type) - - def comma_me(self, amount): - if type(amount) is float: - amount = str('%.2f'%amount) - else: - amount = str(amount) - if (amount == '0'): - return ' ' - orig = amount - new = re.sub("^(-?\d+)(\d{3})", "\g<1>'\g<2>", amount) - if orig == new: - return new - else: - return self.comma_me(new) + "AND account.active ", params) + self.partner_ids = [res['partner_id'] for res in self.cr.dictfetchall()] + objects = obj_partner.browse(self.cr, self.uid, self.partner_ids) + return super(third_party_ledger, self).set_context(objects, data, self.partner_ids, report_type) def lines(self, partner): move_state = ['draft','posted'] @@ -290,105 +266,6 @@ class third_party_ledger(report_sxw.rml_parse, common_report_header): result_tmp = result_tmp + 0.0 return result_tmp + result_init - # code is deprecated -# def _sum_debit(self): -# move_state = ['draft','posted'] -# if self.target_move == 'posted': -# move_state = ['posted'] -# -# if not self.ids: -# return 0.0 -# result_tmp = 0.0 -# result_init = 0.0 -# if self.reconcil: -# RECONCILE_TAG = " " -# else: -# RECONCILE_TAG = "AND reconcile_id IS NULL" -# if self.initial_balance: -# self.cr.execute( -# "SELECT sum(debit) " \ -# "FROM account_move_line AS l, " \ -# "account_move AS m " -# "WHERE partner_id IN %s" \ -# "AND m.id = l.move_id " \ -# "AND m.state IN %s " -# "AND account_id IN %s" \ -# "AND reconcile_id IS NULL " \ -# "AND " + self.init_query + " ", -# (tuple(self.partner_ids), tuple(move_state), tuple(self.account_ids))) -# contemp = self.cr.fetchone() -# if contemp != None: -# result_init = contemp[0] or 0.0 -# else: -# result_init = result_tmp + 0.0 -# -# self.cr.execute( -# "SELECT sum(debit) " \ -# "FROM account_move_line AS l, " \ -# "account_move AS m " -# "WHERE partner_id IN %s" \ -# "AND m.id = l.move_id " \ -# "AND m.state IN %s " -# "AND account_id IN %s" \ -# " " + RECONCILE_TAG + " " \ -# "AND " + self.query + " ", -# (tuple(self.partner_ids), tuple(move_state) ,tuple(self.account_ids),)) -# contemp = self.cr.fetchone() -# if contemp != None: -# result_tmp = contemp[0] or 0.0 -# else: -# result_tmp = result_tmp + 0.0 -# return result_tmp + result_init -# -# def _sum_credit(self): -# move_state = ['draft','posted'] -# if self.target_move == 'posted': -# move_state = ['posted'] -# -# if not self.ids: -# return 0.0 -# result_tmp = 0.0 -# result_init = 0.0 -# if self.reconcil: -# RECONCILE_TAG = " " -# else: -# RECONCILE_TAG = "AND reconcile_id IS NULL" -# if self.initial_balance: -# self.cr.execute( -# "SELECT sum(credit) " \ -# "FROM account_move_line AS l, " \ -# "account_move AS m " -# "WHERE partner_id IN %s" \ -# "AND m.id = l.move_id " \ -# "AND m.state IN %s " -# "AND account_id IN %s" \ -# "AND reconcile_id IS NULL " \ -# "AND " + self.init_query + " ", -# (tuple(self.partner_ids), tuple(move_state), tuple(self.account_ids))) -# contemp = self.cr.fetchone() -# if contemp != None: -# result_init = contemp[0] or 0.0 -# else: -# result_init = result_tmp + 0.0 -# -# self.cr.execute( -# "SELECT sum(credit) " \ -# "FROM account_move_line AS l, " \ -# "account_move AS m " -# "WHERE partner_id IN %s" \ -# "AND m.id = l.move_id " \ -# "AND m.state IN %s " -# "AND account_id IN %s" \ -# " " + RECONCILE_TAG + " " \ -# "AND " + self.query + " ", -# (tuple(self.partner_ids), tuple(move_state), tuple(self.account_ids),)) -# contemp = self.cr.fetchone() -# if contemp != None: -# result_tmp = contemp[0] or 0.0 -# else: -# result_tmp = result_tmp + 0.0 -# return result_tmp + result_init - def _get_partners(self): if self.result_selection == 'customer': return 'Receivable Accounts' diff --git a/addons/account/wizard/account_report_partner_ledger_view.xml b/addons/account/wizard/account_report_partner_ledger_view.xml index f3dd9a80f09..34c5d55e4c1 100644 --- a/addons/account/wizard/account_report_partner_ledger_view.xml +++ b/addons/account/wizard/account_report_partner_ledger_view.xml @@ -23,7 +23,7 @@ - Select Period + Partner Ledger account.partner.ledger ir.actions.act_window form @@ -33,6 +33,13 @@ new + + + + Print Partner Ledger + + + account.analytic.account.search account.analytic.account - + - - - - - - - + + + + + + + + diff --git a/addons/account_voucher/__openerp__.py b/addons/account_voucher/__openerp__.py index d9f1f680030..345c3378aa0 100644 --- a/addons/account_voucher/__openerp__.py +++ b/addons/account_voucher/__openerp__.py @@ -42,7 +42,7 @@ This module manages: 'category': 'Accounting & Finance', 'sequence': 4, 'website' : 'http://openerp.com', - 'images' : ['images/customer_payment.jpeg','images/journal_voucher.jpeg','images/sales_receipt.jpeg','images/supplier_voucher.jpeg'], + 'images' : ['images/customer_payment.jpeg','images/journal_voucher.jpeg','images/sales_receipt.jpeg','images/supplier_voucher.jpeg','images/customer_invoice.jpeg','images/customer_refunds.jpeg'], 'depends' : ['account'], 'demo' : [], 'data' : [ diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index de9cebdd09b..d615eaf7d8c 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -962,7 +962,9 @@ class account_voucher(osv.osv): if not voucher_brw.journal_id.sequence_id.active: raise osv.except_osv(_('Configuration Error !'), _('Please activate the sequence of selected journal !')) - name = seq_obj.next_by_id(cr, uid, voucher_brw.journal_id.sequence_id.id, context=context) + c = dict(context) + c.update({'fiscalyear_id': voucher_brw.period_id.fiscalyear_id.id}) + name = seq_obj.next_by_id(cr, uid, voucher_brw.journal_id.sequence_id.id, context=c) else: raise osv.except_osv(_('Error!'), _('Please define a sequence on the journal.')) @@ -977,7 +979,7 @@ class account_voucher(osv.osv): 'narration': voucher_brw.narration, 'date': voucher_brw.date, 'ref': ref, - 'period_id': voucher_brw.period_id and voucher_brw.period_id.id or False + 'period_id': voucher_brw.period_id.id, } return move diff --git a/addons/account_voucher/account_voucher_data.xml b/addons/account_voucher/account_voucher_data.xml index 440d1c5814a..6329176c6e9 100644 --- a/addons/account_voucher/account_voucher_data.xml +++ b/addons/account_voucher/account_voucher_data.xml @@ -17,6 +17,7 @@ Status Change account.voucher + Status changed diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py index 07552172d9b..005afa5d544 100644 --- a/addons/analytic/analytic.py +++ b/addons/analytic/analytic.py @@ -172,7 +172,7 @@ class account_analytic_account(osv.osv): _columns = { 'name': fields.char('Account/Contract Name', size=128, required=True), - 'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'), + 'complete_name': fields.function(_get_full_name, type='char', string='Full Name'), 'code': fields.char('Reference', select=True), 'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True, help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\ diff --git a/addons/analytic/analytic_data.xml b/addons/analytic/analytic_data.xml index 26eb5ae3dd5..042c2162036 100644 --- a/addons/analytic/analytic_data.xml +++ b/addons/analytic/analytic_data.xml @@ -6,16 +6,19 @@ Contract to Renew account.analytic.account + Contract pending Contract Finished account.analytic.account + Contract closed Contract Opened account.analytic.account + Contract opened diff --git a/addons/anonymization/anonymization.py b/addons/anonymization/anonymization.py index d21b57a901e..de46e2e8d03 100644 --- a/addons/anonymization/anonymization.py +++ b/addons/anonymization/anonymization.py @@ -549,7 +549,7 @@ class ir_model_fields_anonymize_wizard(osv.osv_memory): key = (line['model_id'], line['field_id']) custom_updates = fixes.get(key) if custom_updates: - custom_updates.sort(itemgetter('sequence')) + custom_updates.sort(key=itemgetter('sequence')) queries = [(record['query'], record['query_type']) for record in custom_updates if record['query_type']] elif table_name: queries = [("update %(table)s set %(field)s = %%(value)s where id = %%(id)s" % { diff --git a/addons/auth_signup/auth_signup_data.xml b/addons/auth_signup/auth_signup_data.xml index 32e397b1ba6..31e3b61a9f6 100644 --- a/addons/auth_signup/auth_signup_data.xml +++ b/addons/auth_signup/auth_signup_data.xml @@ -33,5 +33,39 @@

Note: If you do not expect this, you can safely ignore this email.

]]>
+ + + OpenERP Enterprise Connection + + ]]> + ${object.email} + + + + ${object.name}, +

+

+ You have been invited to connect to "${object.company_id.name}" in order to get access to your documents in OpenERP. +

+

+ To accept the invitation, click on the following link: +

+ +

+ Thanks, +

+
+--
+${object.company_id.name or ''}
+${object.company_id.email or ''}
+${object.company_id.phone or ''}
+                    
+ ]]> +
+
+ diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py index b35ff274a6d..9718897ef82 100644 --- a/addons/auth_signup/res_users.py +++ b/addons/auth_signup/res_users.py @@ -246,8 +246,18 @@ class res_users(osv.Model): partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context)] res_partner.signup_prepare(cr, uid, partner_ids, signup_type="reset", expiration=now(days=+1), context=context) + if not context: + context = {} + # send email to users with their signup url - template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email') + template = False + if context.get('create_user'): + try: + template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'set_password_email') + except ValueError: + pass + if not bool(template): + template = self.pool.get('ir.model.data').get_object(cr, uid, 'auth_signup', 'reset_password_email') mail_obj = self.pool.get('mail.mail') assert template._name == 'email.template' for user in self.browse(cr, uid, ids, context): @@ -274,5 +284,6 @@ class res_users(osv.Model): user_id = super(res_users, self).create(cr, uid, values, context=context) user = self.browse(cr, uid, user_id, context=context) if context and context.get('reset_password') and user.email: - user.action_reset_password() + ctx = dict(context, create_user=True) + self.action_reset_password(cr, uid, [user.id], context=ctx) return user_id diff --git a/addons/base_import/static/src/css/import.css b/addons/base_import/static/src/css/import.css index 00c8bd138a4..3913619b6c9 100644 --- a/addons/base_import/static/src/css/import.css +++ b/addons/base_import/static/src/css/import.css @@ -164,7 +164,10 @@ background: #efc9cb; } -.oe_import .select2-results { - font-size: 12px; +/* Field dropdown */ +.oe_import_selector { + font-size: 10px; + /* copied from base.sass:~148 */ + font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; } diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index d29452cc138..3c029335fb1 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -134,6 +134,7 @@ openerp.base_import = function (instance) { start: function () { var self = this; this.setup_encoding_picker(); + this.setup_separator_picker(); return $.when( this._super(), @@ -164,6 +165,26 @@ openerp.base_import = function (instance) { } }).select2('val', 'utf-8'); }, + setup_separator_picker: function () { + this.$('input.oe_import_separator').select2({ + width: '160px', + query: function (q) { + var suggestions = [ + {id: ',', text: _t("Comma")}, + {id: ';', text: _t("Semicolon")}, + {id: '\t', text: _t("Tab")}, + {id: ' ', text: _t("Space")} + ]; + if (q.term) { + suggestions.unshift({id: q.term, text: q.term}); + } + q.callback({results: suggestions}); + }, + initSelection: function (e, c) { + return c({id: ',', text: _t("Comma")}); + }, + }); + }, import_options: function () { var self = this; @@ -336,8 +357,18 @@ openerp.base_import = function (instance) { var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) { return $(el).select2('val') || false; }).get(); - return this.Import.call( - 'do', [this.id, fields, this.import_options()], options); + return this.Import.call('do', [this.id, fields, this.import_options()], options) + .then(undefined, function (error, event) { + // In case of unexpected exception, convert + // "JSON-RPC error" to an import failure, and + // prevent default handling (warning dialog) + if (event) { event.preventDefault(); } + return $.when([{ + type: 'error', + record: false, + message: error.data.fault_code, + }]); + }) ; }, onvalidate: function () { return this.call_import({ dryrun: true }) diff --git a/addons/contacts/__openerp__.py b/addons/contacts/__openerp__.py index 3b4df183478..3fca504cbc8 100644 --- a/addons/contacts/__openerp__.py +++ b/addons/contacts/__openerp__.py @@ -36,6 +36,7 @@ You can track your suppliers, customers and other contacts. 'data': [ 'contacts_view.xml', ], + 'images': ['images/contacts.jpeg'], 'installable': True, 'application': True, 'auto_install': False, diff --git a/addons/crm/__openerp__.py b/addons/crm/__openerp__.py index 4a792abc9ee..0dd075d48e6 100644 --- a/addons/crm/__openerp__.py +++ b/addons/crm/__openerp__.py @@ -54,6 +54,7 @@ Dashboard for CRM will include: 'base_status', 'process', 'mail', + 'email_template', 'base_calendar', 'resource', 'board', @@ -116,6 +117,6 @@ Dashboard for CRM will include: 'installable': True, 'application': True, 'auto_install': False, - 'images': ['images/sale_crm_crm_dashboard.png', 'images/crm_dashboard.jpeg','images/leads.jpeg','images/meetings.jpeg','images/opportunities.jpeg','images/outbound_calls.jpeg','images/stages.jpeg'], + 'images': ['images/crm_dashboard.png', 'images/customers.png','images/leads.png','images/opportunities_kanban.png','images/opportunities_form.png','images/opportunities_calendar.png','images/opportunities_graph.png','images/logged_calls.png','images/scheduled_calls.png','images/stages.png'], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 4455663ca9a..52e9e4ed942 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -36,7 +36,6 @@ CRM_LEAD_FIELDS_TO_MERGE = ['name', 'company_id', 'country_id', 'section_id', - 'stage_id', 'state_id', 'type_id', 'user_id', @@ -472,6 +471,15 @@ class crm_lead(base_stage, format_address, osv.osv): return 'lead' + def _merge_get_result_stage(self, cr, uid, opps, context=None): + stage = None + for opp in opps: + if not stage: + stage = opp.stage_id.id + if opp.type == 'opportunity': + return opp.stage_id.id + return stage + def _merge_data(self, cr, uid, ids, oldest, fields, context=None): """ Prepare lead/opp data into a dictionary for merging. Different types @@ -488,10 +496,8 @@ class crm_lead(base_stage, format_address, osv.osv): opportunities = self.browse(cr, uid, ids, context=context) def _get_first_not_null(attr): - if hasattr(oldest, attr): - return getattr(oldest, attr) for opp in opportunities: - if hasattr(opp, attr): + if hasattr(opp, attr) and bool(getattr(opp, attr)): return getattr(opp, attr) return False @@ -520,7 +526,7 @@ class crm_lead(base_stage, format_address, osv.osv): # Define the resulting type ('lead' or 'opportunity') data['type'] = self._merge_get_result_type(cr, uid, opportunities, context) - + data['stage_id'] = self._merge_get_result_stage(cr, uid, opportunities, context) return data def _merge_find_oldest(self, cr, uid, ids, context=None): @@ -533,9 +539,6 @@ class crm_lead(base_stage, format_address, osv.osv): if context is None: context = {} - if context.get('convert'): - ids = list(set(ids) - set(context.get('lead_ids', []))) - # Search opportunities order by create date opportunity_ids = self.search(cr, uid, [('id', 'in', ids)], order='create_date', context=context) oldest_opp_id = opportunity_ids[0] @@ -643,19 +646,11 @@ class crm_lead(base_stage, format_address, osv.osv): if len(ids) <= 1: raise osv.except_osv(_('Warning!'),_('Please select more than one element (lead or opportunity) from the list view.')) - - lead_ids = context.get('lead_ids', []) - - ctx_opportunities = self.browse(cr, uid, lead_ids, context=context) - opportunities = self.browse(cr, uid, ids, context=context) - opportunities_list = list(set(opportunities) - set(ctx_opportunities)) + ids.sort() oldest = self._merge_find_oldest(cr, uid, ids, context=context) - if ctx_opportunities: - first_opportunity = ctx_opportunities[0] - tail_opportunities = opportunities_list + ctx_opportunities[1:] - else: - first_opportunity = opportunities_list[0] - tail_opportunities = opportunities_list[1:] + opportunities_rest = self.browse(cr, uid, list(set(ids) - set([oldest.id])), context=context) + first_opportunity = oldest + tail_opportunities = opportunities_rest merged_data = self._merge_data(cr, uid, ids, oldest, CRM_LEAD_FIELDS_TO_MERGE, context=context) @@ -664,14 +659,14 @@ class crm_lead(base_stage, format_address, osv.osv): self._merge_opportunity_attachments(cr, uid, first_opportunity.id, tail_opportunities, context=context) # Merge notifications about loss of information + opportunities = [oldest] + opportunities.extend(opportunities_rest) self._merge_notify(cr, uid, first_opportunity, opportunities, context=context) # Write merged data into first opportunity self.write(cr, uid, [first_opportunity.id], merged_data, context=context) # Delete tail opportunities self.unlink(cr, uid, [x.id for x in tail_opportunities], context=context) - # Open first opportunity - self.case_open(cr, uid, [first_opportunity.id]) return first_opportunity.id def _convert_opportunity_data(self, cr, uid, lead, customer, section_id=False, context=None): @@ -716,7 +711,7 @@ class crm_lead(base_stage, format_address, osv.osv): def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None): partner = self.pool.get('res.partner') - vals = { 'name': name, + vals = {'name': name, 'user_id': lead.user_id.id, 'comment': lead.description, 'section_id': lead.section_id.id or False, @@ -736,11 +731,11 @@ class crm_lead(base_stage, format_address, osv.osv): 'is_company': is_company, 'type': 'contact' } - partner = partner.create(cr, uid,vals, context) + partner = partner.create(cr, uid, vals, context=context) return partner def _create_lead_partner(self, cr, uid, lead, context=None): - partner_id = False + partner_id = False if lead.partner_name and lead.contact_name: partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context) partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context) @@ -748,8 +743,14 @@ class crm_lead(base_stage, format_address, osv.osv): partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context) elif not lead.partner_name and lead.contact_name: partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, context=context) + elif lead.email_from and self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0]: + contact_name = self.pool.get('res.partner')._parse_partner_name(lead.email_from, context=context)[0] + partner_id = self._lead_create_contact(cr, uid, lead, contact_name, False, context=context) else: - partner_id = self._lead_create_contact(cr, uid, lead, lead.name, False, context=context) + raise osv.except_osv( + _('Warning!'), + _('No customer name defined. Please fill one of the following fields: Company Name, Contact Name or Email ("Name ")') + ) return partner_id def _lead_set_partner(self, cr, uid, lead, partner_id, context=None): @@ -763,7 +764,7 @@ class crm_lead(base_stage, format_address, osv.osv): res = False res_partner = self.pool.get('res.partner') if partner_id: - res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id.id or False}) + res_partner.write(cr, uid, partner_id, {'section_id': lead.section_id and lead.section_id.id or False}) contact_id = res_partner.address_get(cr, uid, [partner_id])['default'] res = lead.write({'partner_id': partner_id}, context=context) message = _("Partner set to %s." % (lead.partner_id.name)) @@ -872,8 +873,8 @@ class crm_lead(base_stage, format_address, osv.osv): 'res_id': int(opportunity_id), 'view_id': False, 'views': [(form_view or False, 'form'), - (tree_view or False, 'tree'), - (False, 'calendar'), (False, 'graph')], + (tree_view or False, 'tree'), + (False, 'calendar'), (False, 'graph')], 'type': 'ir.actions.act_window', } @@ -980,8 +981,11 @@ class crm_lead(base_stage, format_address, osv.osv): 'description': desc, 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), + 'partner_id': msg.get('author_id', False), 'user_id': False, } + if msg.get('author_id'): + defaults.update(self.on_change_partner(cr, uid, None, msg.get('author_id'), context=context)['value']) if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES): defaults['priority'] = msg.get('priority') defaults.update(custom_values) diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml index bb590f49d73..724ec491efe 100644 --- a/addons/crm/crm_lead_data.xml +++ b/addons/crm/crm_lead_data.xml @@ -171,11 +171,13 @@ Stage Changed crm.lead + Stage changed Opportunity Won crm.lead + Opportunity won diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 3d7bd45a209..3e9249aa6fa 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -168,7 +168,7 @@ - + @@ -219,12 +219,14 @@ + + @@ -235,6 +237,7 @@ + CRM - Leads Calendar @@ -322,35 +325,39 @@ - - - - - - - - - + - + + + + + + + + + - - - - - - - - + + + + + + + + + + - - + + - +
@@ -517,6 +524,7 @@ + @@ -531,35 +539,41 @@ crm.lead - + - - - - - - - - - + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + diff --git a/addons/crm/crm_meeting.py b/addons/crm/crm_meeting.py index 3d1fc36b8dd..14b69ed0e11 100644 --- a/addons/crm/crm_meeting.py +++ b/addons/crm/crm_meeting.py @@ -68,24 +68,4 @@ class calendar_attendee(osv.osv): relation="crm.case.categ", multi='categ_id'), } -class res_users(osv.osv): - _name = 'res.users' - _inherit = 'res.users' - - def create(self, cr, uid, data, context=None): - user_id = super(res_users, self).create(cr, uid, data, context=context) - - # add shortcut unless 'noshortcut' is True in context - if not(context and context.get('noshortcut', False)): - data_obj = self.pool.get('ir.model.data') - try: - data_id = data_obj._get_id(cr, uid, 'crm', 'ir_ui_view_sc_calendar0') - view_id = data_obj.browse(cr, uid, data_id, context=context).res_id - self.pool.get('ir.ui.view_sc').copy(cr, uid, view_id, default = { - 'user_id': user_id}, context=context) - except: - # Tolerate a missing shortcut. See product/product.py for similar code. - _logger.debug('Skipped meetings shortcut for user "%s".', data.get('name','' stage_id: stage_lead1 - !record {model: crm.lead, id: test_crm_lead_02}: type: 'lead' name: 'Test lead 2' + email_from: 'Raoul Grosbedon ' stage_id: stage_lead1 - !record {model: crm.lead, id: test_crm_lead_03}: type: 'lead' name: 'Test lead 3' + email_from: 'Raoul Grosbedon ' stage_id: stage_lead1 - !record {model: crm.lead, id: test_crm_lead_04}: type: 'lead' name: 'Test lead 4' + contact_name: 'Fabrice Lepoilu' stage_id: stage_lead1 - !record {model: crm.lead, id: test_crm_lead_05}: type: 'lead' name: 'Test lead 5' + contact_name: 'Fabrice Lepoilu' stage_id: stage_lead1 - !record {model: crm.lead, id: test_crm_lead_06}: type: 'lead' name: 'Test lead 6' + partner_name: 'Agrolait SuperSeed SA' stage_id: stage_lead1 - !record {model: res.users, id: test_res_user_01}: diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index e7129c98d6c..32396b95f4e 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -34,7 +34,7 @@ class crm_lead2opportunity_partner(osv.osv_memory): ('convert', 'Convert to opportunity'), ('merge', 'Merge with existing opportunities') ], 'Conversion Action', required=True), - 'opportunity_ids': fields.many2many('crm.lead', string='Opportunities', domain=[('type', '=', 'opportunity')]), + 'opportunity_ids': fields.many2many('crm.lead', string='Opportunities'), } def default_get(self, cr, uid, fields, context=None): @@ -46,42 +46,34 @@ class crm_lead2opportunity_partner(osv.osv_memory): lead_obj = self.pool.get('crm.lead') res = super(crm_lead2opportunity_partner, self).default_get(cr, uid, fields, context=context) - opportunities = res.get('opportunity_ids') or [] - partner_id = False - email = False - for lead in lead_obj.browse(cr, uid, opportunities, context=context): - partner_id = lead.partner_id and lead.partner_id.id or False + if context.get('active_id'): + tomerge = set([int(context['active_id'])]) + + email = False + partner_id = res.get('partner_id') + lead = lead_obj.browse(cr, uid, int(context['active_id']), context=context) #TOFIX: use mail.mail_message.to_mail email = re.findall(r'([^ ,<@]+@[^> ,]+)', lead.email_from or '') - email = map(lambda x: "'" + x + "'", email) - if not partner_id and res.get('partner_id'): - partner_id = res.get('partner_id') - - ids = [] - if partner_id: - # Search for opportunities that have the same partner and that arent done or cancelled - ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), ('type', '=', 'opportunity'), '!', ('state', 'in', ['done', 'cancel'])]) - if ids: - opportunities.append(ids[0]) - if not partner_id: + if partner_id: + # Search for opportunities that have the same partner and that arent done or cancelled + ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id)]) + for id in ids: + tomerge.add(id) if email: - # Find email of existing opportunity matching the email_from of the lead - cr.execute("""select id from crm_lead where type='opportunity' and - substring(email_from from '([^ ,<@]+@[^> ,]+)') in (%s)""" % (','.join(email))) - ids = map(lambda x:x[0], cr.fetchall()) - if ids: - opportunities.append(ids[0]) + ids = lead_obj.search(cr, uid, [('email_from', 'ilike', email[0])]) + for id in ids: + tomerge.add(id) - if 'action' in fields: - res.update({'action' : partner_id and 'exist' or 'create'}) - if 'partner_id' in fields: - res.update({'partner_id' : partner_id}) - if 'name' in fields: - res.update({'name' : ids and 'merge' or 'convert'}) - if 'opportunity_ids' in fields: - res.update({'opportunity_ids': opportunities}) + if 'action' in fields: + res.update({'action' : partner_id and 'exist' or 'create'}) + if 'partner_id' in fields: + res.update({'partner_id' : partner_id}) + if 'name' in fields: + res.update({'name' : len(tomerge) >= 2 and 'merge' or 'convert'}) + if 'opportunity_ids' in fields and len(tomerge) >= 2: + res.update({'opportunity_ids': list(tomerge)}) return res @@ -115,38 +107,44 @@ class crm_lead2opportunity_partner(osv.osv_memory): lead.allocate_salesman(cr, uid, lead_ids, user_ids, team_id=team_id, context=context) return res - def _merge_opportunity(self, cr, uid, ids, opportunity_ids, action='merge', context=None): - if context is None: - context = {} - res = False - # Expected: all newly-converted leads (active_ids) will be merged with the opportunity(ies) - # that have been selected in the 'opportunity_ids' m2m, with all these records - # merged into the first opportunity (and the rest deleted) - opportunity_ids = [o.id for o in opportunity_ids] - lead_ids = context.get('active_ids', []) - if action == 'merge' and lead_ids and opportunity_ids: - # Add the leads in the to-merge list, next to other opps - # (the fact that they're passed in context['lead_ids'] means that - # they cannot be selected to contain the result of the merge. - opportunity_ids.extend(lead_ids) - context.update({'lead_ids': lead_ids, "convert" : True}) - res = self.pool.get('crm.lead').merge_opportunity(cr, uid, opportunity_ids, context=context) - return res - def action_apply(self, cr, uid, ids, context=None): """ Convert lead to opportunity or merge lead and opportunity and open the freshly created opportunity view. """ + if context is None: + context = {} + + w = self.browse(cr, uid, ids, context=context)[0] + opp_ids = [o.id for o in w.opportunity_ids] + if w.name == 'merge': + lead_id = self.pool.get('crm.lead').merge_opportunity(cr, uid, opp_ids, context=context) + lead_ids = [lead_id] + lead = self.pool.get('crm.lead').read(cr, uid, lead_id, ['type'], context=context) + if lead['type'] == "lead": + context.update({'active_ids': lead_ids}) + self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context) + else: + lead_ids = context.get('active_ids', []) + self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context) + + return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context) + + def _create_partner(self, cr, uid, ids, context=None): + """ + Create partner based on action. + :return dict: dictionary organized as followed: {lead_id: partner_assigned_id} + """ + #TODO this method in only called by crm_lead2opportunity_partner + #wizard and would probably diserve to be refactored or at least + #moved to a better place if context is None: context = {} lead = self.pool.get('crm.lead') lead_ids = context.get('active_ids', []) data = self.browse(cr, uid, ids, context=context)[0] - self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids}, context=context) - self._merge_opportunity(cr, uid, ids, data.opportunity_ids, data.name, context=context) - return lead.redirect_opportunity_view(cr, uid, lead_ids[0], context=context) - + partner_id = data.partner_id and data.partner_id.id or False + return lead.handle_partner_assignation(cr, uid, lead_ids, data.action, partner_id, context=context) class crm_lead2opportunity_mass_convert(osv.osv_memory): _name = 'crm.lead2opportunity.partner.mass' diff --git a/addons/crm/wizard/crm_lead_to_opportunity_view.xml b/addons/crm/wizard/crm_lead_to_opportunity_view.xml index 1a7bd687a94..8bfd60fccec 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity_view.xml +++ b/addons/crm/wizard/crm_lead_to_opportunity_view.xml @@ -9,11 +9,19 @@
- + + + + - - + + + + + + + @@ -51,9 +59,15 @@ + - - + + + + + + + diff --git a/addons/crm/wizard/crm_merge_opportunities_view.xml b/addons/crm/wizard/crm_merge_opportunities_view.xml index 4b035200737..1dc98b6b68f 100644 --- a/addons/crm/wizard/crm_merge_opportunities_view.xml +++ b/addons/crm/wizard/crm_merge_opportunities_view.xml @@ -11,9 +11,14 @@ + - - + + + + + + diff --git a/addons/crm/wizard/crm_partner_binding.py b/addons/crm/wizard/crm_partner_binding.py index efef0fa2611..cb2184dca2a 100644 --- a/addons/crm/wizard/crm_partner_binding.py +++ b/addons/crm/wizard/crm_partner_binding.py @@ -96,20 +96,4 @@ class crm_partner_binding(osv.osv_memory): return res - def _create_partner(self, cr, uid, ids, context=None): - """ - Create partner based on action. - :return dict: dictionary organized as followed: {lead_id: partner_assigned_id} - """ - #TODO this method in only called by crm_lead2opportunity_partner - #wizard and would probably diserve to be refactored or at least - #moved to a better place - if context is None: - context = {} - lead = self.pool.get('crm.lead') - lead_ids = context.get('active_ids', []) - data = self.browse(cr, uid, ids, context=context)[0] - partner_id = data.partner_id and data.partner_id.id or False - return lead.handle_partner_assignation(cr, uid, lead_ids, data.action, partner_id, context=context) - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py index a724579f1e8..275d9edaab4 100644 --- a/addons/crm_claim/crm_claim.py +++ b/addons/crm_claim/crm_claim.py @@ -195,6 +195,7 @@ class crm_claim(base_stage, osv.osv): 'description': desc, 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), + 'partner_id': msg.get('author_id', False), } if msg.get('priority'): defaults['priority'] = msg.get('priority') diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py index 5ed1c040627..2d0383962b1 100644 --- a/addons/crm_helpdesk/crm_helpdesk.py +++ b/addons/crm_helpdesk/crm_helpdesk.py @@ -106,6 +106,7 @@ class crm_helpdesk(base_state, base_stage, osv.osv): 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), 'user_id': False, + 'partner_id': msg.get('author_id', False), } defaults.update(custom_values) return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=defaults, context=context) diff --git a/addons/event/__openerp__.py b/addons/event/__openerp__.py index aa9f209df88..54b77c82f69 100644 --- a/addons/event/__openerp__.py +++ b/addons/event/__openerp__.py @@ -57,6 +57,6 @@ Key Features 'installable': True, 'application': True, 'auto_install': False, - 'images': ['images/1_event_type_list.jpeg','images/2_events.jpeg','images/3_registrations.jpeg'], + 'images': ['images/1_event_type_list.jpeg','images/2_events.jpeg','images/3_registrations.jpeg','images/events_kanban.jpeg'], } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/fleet/__openerp__.py b/addons/fleet/__openerp__.py index 681f4290bcf..fc7649de20e 100644 --- a/addons/fleet/__openerp__.py +++ b/addons/fleet/__openerp__.py @@ -54,6 +54,7 @@ Main Features 'fleet_data.xml', 'fleet_board_view.xml', ], + 'images': ['images/costs_analysis.jpeg','images/indicative_costs_analysis.jpeg','images/vehicles.jpeg','images/vehicles_contracts.jpeg','images/vehicles_fuel.jpeg','images/vehicles_odometer.jpeg','images/vehicles_services.jpeg'], 'update_xml' : ['security/fleet_security.xml','security/ir.model.access.csv'], 'demo': ['fleet_demo.xml'], diff --git a/addons/google_docs/google_docs.py b/addons/google_docs/google_docs.py index 4b98798c832..cdb14de1de3 100644 --- a/addons/google_docs/google_docs.py +++ b/addons/google_docs/google_docs.py @@ -29,13 +29,40 @@ _logger = logging.getLogger(__name__) try: import gdata.docs.data import gdata.docs.client - from gdata.client import RequestError - from gdata.docs.service import DOCUMENT_LABEL - import gdata.auth - from gdata.docs.data import Resource + + # API breakage madness in the gdata API - those guys are insane. + try: + # gdata 2.0.15+ + gdata.docs.client.DocsClient.copy_resource + except AttributeError: + # gdata 2.0.14- : copy_resource() was copy() + gdata.docs.client.DocsClient.copy_resource = gdata.docs.client.DocsClient.copy + + try: + # gdata 2.0.16+ + gdata.docs.client.DocsClient.get_resource_by_id + except AttributeError: + try: + # gdata 2.0.15+ + gdata.docs.client.DocsClient.get_resource_by_self_link + def get_resource_by_id_2_0_16(self, resource_id, **kwargs): + return self.GetResourceBySelfLink( + gdata.docs.client.RESOURCE_FEED_URI + ('/%s' % resource_id), **kwargs) + gdata.docs.client.DocsClient.get_resource_by_id = get_resource_by_id_2_0_16 + except AttributeError: + # gdata 2.0.14- : alias get_resource_by_id() + gdata.docs.client.DocsClient.get_resource_by_id = gdata.docs.client.DocsClient.get_doc + + try: + import atom.http_interface + _logger.info('GData lib version `%s` detected' % atom.http_interface.USER_AGENT) + except (ImportError, AttributeError): + _logger.debug('GData lib version could not be detected', exc_info=True) + except ImportError: _logger.warning("Please install latest gdata-python-client from http://code.google.com/p/gdata-python-client/downloads/list") + class google_docs_ir_attachment(osv.osv): _inherit = 'ir.attachment' @@ -49,11 +76,12 @@ class google_docs_ir_attachment(osv.osv): #get gmail password and login. We use default_get() instead of a create() followed by a read() on the # google.login object, because it is easier. The keys 'user' and 'password' ahve to be passed in the dict # but the values will be replaced by the user gmail password and login. - user_config = google_pool.default_get( cr, uid, {'user' : '' , 'password' : ''}, context=context) + user_config = google_pool.default_get(cr, uid, {'user' : '' , 'password' : ''}, context=context) #login gmail account - client = google_pool.google_login( user_config['user'], user_config['password'], type='docs_client', context=context) + client = google_pool.google_login(user_config['user'], user_config['password'], type='docs_client', context=context) if not client: - raise osv.except_osv( _('Google Docs Error!'), _("Check your google configuration in Users/Users/Synchronization tab.")) + raise osv.except_osv(_('Google Docs Error!'), _("Check your google configuration in Users/Users/Synchronization tab.")) + _logger.info('Logged into google docs as %s', user_config['user']) return client def create_empty_google_doc(self, cr, uid, res_model, res_id, context=None): @@ -94,9 +122,9 @@ class google_docs_ir_attachment(osv.osv): client = self._auth(cr, uid) # fetch and copy the original document try: - doc = client.GetDoc(gdoc_template_id) + doc = client.get_resource_by_id(gdoc_template_id) #copy the document you choose in the configuration - copy_resource = client.copy(doc, name_gdocs) + copy_resource = client.copy_resource(doc, name_gdocs) except: raise osv.except_osv(_('Google Docs Error!'), _("Your resource id is not correct. You can find the id in the google docs URL.")) # create an ir.attachment diff --git a/addons/hr_evaluation/__openerp__.py b/addons/hr_evaluation/__openerp__.py index fb303ff28e4..0c999ec4f7b 100644 --- a/addons/hr_evaluation/__openerp__.py +++ b/addons/hr_evaluation/__openerp__.py @@ -27,7 +27,7 @@ 'sequence': 31, 'website': 'http://www.openerp.com', 'summary': 'Periodical Evaluations, Appraisals, Surveys', - 'images': ['images/hr_evaluation_analysis.jpeg','images/hr_evaluation.jpeg'], + 'images': ['images/hr_evaluation_analysis.jpeg','images/hr_evaluation.jpeg','images/hr_interview_requests.jpeg'], 'depends': ['hr','base_calendar','survey'], 'description': """ Periodical Employees evaluation and appraisals diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index 44d8912c5f1..92465c1b84e 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -353,6 +353,7 @@ class hr_applicant(base_stage, osv.Model): 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), 'user_id': False, + 'partner_id': msg.get('author_id', False), } if msg.get('priority'): defaults['priority'] = msg.get('priority') diff --git a/addons/hr_recruitment/hr_recruitment_data.xml b/addons/hr_recruitment/hr_recruitment_data.xml index 13736d1442a..a0b16f042bc 100644 --- a/addons/hr_recruitment/hr_recruitment_data.xml +++ b/addons/hr_recruitment/hr_recruitment_data.xml @@ -470,11 +470,13 @@ Stage Changed hr.applicant + Stage changed Applicant Hired hr.applicant + Applicant hired diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py index 5f988439b50..e92ab4400bd 100644 --- a/addons/hr_timesheet_sheet/__openerp__.py +++ b/addons/hr_timesheet_sheet/__openerp__.py @@ -45,7 +45,7 @@ The validation can be configured in the company: """, 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', - 'images': ['images/hr_my_timesheet.jpeg','images/hr_timesheet_analysis.jpeg','images/hr_timesheet_sheet_analysis.jpeg','images/hr_timesheets.jpeg'], + 'images': ['images/hr_my_current_timesheet.jpeg','images/hr_timesheet_analysis.jpeg','images/hr_timesheet_sheet_analysis.jpeg','images/hr_timesheet_activity.jpeg'], 'depends': ['hr_timesheet', 'hr_timesheet_invoice', 'process'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/lunch/__openerp__.py b/addons/lunch/__openerp__.py index a70f6d82f9e..46c07c211c6 100644 --- a/addons/lunch/__openerp__.py +++ b/addons/lunch/__openerp__.py @@ -44,6 +44,7 @@ If you want to save your employees' time and avoid them to always have coins in 'report/report_lunch_order_view.xml', 'security/ir.model.access.csv',], 'css':['static/src/css/lunch.css'], + 'images': ['images/new_order.jpeg','images/lunch_account.jpeg','images/order_by_supplier_analysis.jpeg','images/alert.jpeg'], 'demo': ['lunch_demo.xml',], 'installable': True, 'application' : True, diff --git a/addons/lunch/lunch_view.xml b/addons/lunch/lunch_view.xml index 8974099a294..dd387620102 100644 --- a/addons/lunch/lunch_view.xml +++ b/addons/lunch/lunch_view.xml @@ -39,7 +39,8 @@ - + + @@ -116,7 +117,7 @@ lunch.cashmove tree - {"search_default_is_mine":1} + {"search_default_is_mine_group":1}

Here you can see your cash moves.
A cash moves can be either an expense or a payment. diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 12fdc62c260..63cd0888986 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -71,16 +71,12 @@ Main Features 'installable': True, 'application': True, 'images': [ - 'images/customer_history.jpeg', + 'images/inbox.jpeg', 'images/messages_form.jpeg', 'images/messages_list.jpeg', - 'static/src/img/email_icong.png', - 'static/src/img/_al.png', - 'static/src/img/_pincky.png', - 'static/src/img/groupdefault.png', - 'static/src/img/attachment.png', - 'static/src/img/checklist.png', - 'static/src/img/formatting.png', + 'images/email.jpeg', + 'images/join_a_group.jpeg', + 'images/share_a_message.jpeg', ], 'css': [ 'static/src/css/mail.css', diff --git a/addons/mail/doc/mail_message.rst b/addons/mail/doc/mail_message.rst index 02baf6c4708..b5e2d8da8b4 100644 --- a/addons/mail/doc/mail_message.rst +++ b/addons/mail/doc/mail_message.rst @@ -21,6 +21,8 @@ should inherit from this class. ClientAction (ir.actions.client) ++++++++++++++++++++++++++++++++ +.. code-block:: xml + Inbox mail.wall @@ -36,6 +38,7 @@ ClientAction (ir.actions.client) 'mail_thread' widget for field on standard view. (default value like a thread for record, view on flat mode, no reply, no read/unread) 'mail.widget' it's the root thread, used by 'mail.wall' and 'mail_thread' + - ``help`` : Text HTML to display if there are no message - ``context`` : insert 'default_model' and 'default_res_id' - ``params`` : options for the widget diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index ec9bc86d6c5..a9d33f4585d 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -85,9 +85,6 @@ class mail_notification(osv.Model): if notification.read: continue partner = notification.partner_id - # Do not send an email to the writer - if partner.user_ids and partner.user_ids[0].id == uid: - continue # Do not send to partners without email address defined if not partner.email: continue @@ -129,11 +126,20 @@ class mail_notification(osv.Model): if signature: body_html = tools.append_content_to_html(body_html, signature, plaintext=True, container_tag='div') + # email_from: partner-user alias or partner email or mail.message email_from + if msg.author_id and msg.author_id.user_ids and msg.author_id.user_ids[0].alias_domain and msg.author_id.user_ids[0].alias_name: + email_from = '%s <%s@%s>' % (msg.author_id.name, msg.author_id.user_ids[0].alias_name, msg.author_id.user_ids[0].alias_domain) + elif msg.author_id: + email_from = '%s <%s>' % (msg.author_id.name, msg.author_id.email) + else: + email_from = msg.email_from + mail_values = { 'mail_message_id': msg.id, 'email_to': [], 'auto_delete': True, 'body_html': body_html, + 'email_from': email_from, 'state': 'outgoing', } mail_values['email_to'] = ', '.join(mail_values['email_to']) diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 7b8ec0b3579..eb5ec30496e 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -78,6 +78,13 @@ class mail_mail(osv.Model): 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx), } + def default_get(self, cr, uid, fields, context=None): + # protection for `default_type` values leaking from menu action context (e.g. for invoices) + # To remove when automatic context propagation is removed in web client + if context and context.get('default_type') and context.get('default_type') not in self._all_columns['type'].column.selection: + context = dict(context, default_type = None) + return super(mail_mail, self).default_get(cr, uid, fields, context=context) + def create(self, cr, uid, values, context=None): if 'notification' not in values and values.get('mail_message_id'): values['notification'] = True diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index d75a9199369..9d7237ca907 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -243,7 +243,7 @@ class mail_thread(osv.AbstractModel): # subscribe uid unless asked not to if not context.get('mail_create_nosubscribe'): self.message_subscribe_users(cr, uid, [thread_id], [uid], context=context) - self.message_subscribe_from_parent(cr, uid, [thread_id], values.keys(), context=context) + self.message_auto_subscribe(cr, uid, [thread_id], values.keys(), context=context) # automatic logging unless asked not to (mainly for various testing purpose) if not context.get('mail_create_nolog'): @@ -261,7 +261,7 @@ class mail_thread(osv.AbstractModel): # Perform write, update followers result = super(mail_thread, self).write(cr, uid, ids, values, context=context) - self.message_subscribe_from_parent(cr, uid, ids, values.keys(), context=context) + self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context) # Perform the tracking if tracked_fields: @@ -1069,7 +1069,24 @@ class mail_thread(osv.AbstractModel): self.check_access_rights(cr, uid, 'write') return self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context) - def message_subscribe_from_parent(self, cr, uid, ids, updated_fields, context=None): + def _message_get_auto_subscribe_fields(self, cr, uid, updated_fields, auto_follow_fields=['user_id'], context=None): + """ Returns the list of relational fields linking to res.users that should + trigger an auto subscribe. The default list checks for the fields + - called 'user_id' + - linking to res.users + - with track_visibility set + In OpenERP V7, this is sufficent for all major addon such as opportunity, + project, issue, recruitment, sale. + Override this method if a custom behavior is needed about fields + that automatically subscribe users. + """ + user_field_lst = [] + for name, column_info in self._all_columns.items(): + if name in auto_follow_fields and name in updated_fields and getattr(column_info.column, 'track_visibility', False) and column_info.column._obj == 'res.users': + user_field_lst.append(name) + return user_field_lst + + def message_auto_subscribe(self, cr, uid, ids, updated_fields, context=None): """ 1. fetch project subtype related to task (parent_id.res_model = 'project.task') 2. for each project subtype: subscribe the follower to the task @@ -1077,13 +1094,16 @@ class mail_thread(osv.AbstractModel): subtype_obj = self.pool.get('mail.message.subtype') follower_obj = self.pool.get('mail.followers') + # fetch auto_follow_fields + user_field_lst = self._message_get_auto_subscribe_fields(cr, uid, updated_fields, context=context) + # fetch related record subtypes related_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('parent_id.res_model', '=', self._name)], context=context) subtypes = subtype_obj.browse(cr, uid, related_subtype_ids, context=context) default_subtypes = [subtype for subtype in subtypes if subtype.res_model == False] related_subtypes = [subtype for subtype in subtypes if subtype.res_model != False] relation_fields = set([subtype.relation_field for subtype in subtypes if subtype.relation_field != False]) - if not related_subtypes or not any(relation in updated_fields for relation in relation_fields): + if (not related_subtypes or not any(relation in updated_fields for relation in relation_fields)) and not user_field_lst: return True for record in self.browse(cr, uid, ids, context=context): @@ -1105,20 +1125,24 @@ class mail_thread(osv.AbstractModel): for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context): new_followers.setdefault(follower.partner_id.id, set()).add(subtype.parent_id.id) - if not parent_res_id or not parent_model: - continue + if parent_res_id and parent_model: + for subtype in default_subtypes: + follower_ids = follower_obj.search(cr, SUPERUSER_ID, [ + ('res_model', '=', parent_model), + ('res_id', '=', parent_res_id), + ('subtype_ids', 'in', [subtype.id]) + ], context=context) + for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context): + new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id) - for subtype in default_subtypes: - follower_ids = follower_obj.search(cr, SUPERUSER_ID, [ - ('res_model', '=', parent_model), - ('res_id', '=', parent_res_id), - ('subtype_ids', 'in', [subtype.id]) - ], context=context) - for follower in follower_obj.browse(cr, SUPERUSER_ID, follower_ids, context=context): - new_followers.setdefault(follower.partner_id.id, set()).add(subtype.id) + # add followers coming from res.users relational fields that are tracked + user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)] + for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]: + new_followers.setdefault(partner_id, None) for pid, subtypes in new_followers.items(): - self.message_subscribe(cr, uid, [record.id], [pid], list(subtypes), context=context) + subtypes = list(subtypes) if subtypes is not None else None + self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context) return True #------------------------------------------------------ diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index e3986a390d6..0b3bddffde1 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -105,7 +105,7 @@ openerp.mail = function (session) { // As it only looks at the extension it is quite approximative. filetype: function(url){ url = url.filename || url; - var tokens = url.split('.'); + var tokens = (url+'').split('.'); if(tokens.length <= 1){ return 'unknown'; } @@ -218,7 +218,7 @@ openerp.mail = function (session) { this.author_id = datasets.author_id || false, this.attachment_ids = datasets.attachment_ids || [], this.partner_ids = datasets.partner_ids || []; - this._date = datasets.date; + this.date = datasets.date; this.format_data(); @@ -232,19 +232,20 @@ openerp.mail = function (session) { else { this.options.show_read = this.to_read; this.options.show_unread = !this.to_read; - this.options.rerender = true; - this.options.toggle_read = true; } + this.options.rerender = true; + this.options.toggle_read = true; } - this.parent_thread = parent.messages != undefined ? parent : this.options.root_thread; + this.parent_thread = typeof parent.on_message_detroy == 'function' ? parent : this.options.root_thread; this.thread = false; }, /* Convert date, timerelative and avatar in displayable data. */ format_data: function () { //formating and add some fields for render - if (this._date) { - this.timerelative = $.timeago(this._date+"Z"); + this.date = this.date ? session.web.str_to_datetime(this.date) : false; + if (this.date && new Date().getTime()-this.date.getTime() < 7*24*60*60*1000) { + this.timerelative = $.timeago(this.date); } if (this.type == 'email' && (!this.author_id || !this.author_id[0])) { this.avatar = ('/mail/static/src/img/email_icon.png'); @@ -253,7 +254,7 @@ openerp.mail = function (session) { } else { this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); } - if (this.author_id) { + if (this.author_id && this.author_id[1]) { var email = this.author_id[1].match(/(.*)<(.*@.*)>/); if (!email) { this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]); @@ -271,7 +272,7 @@ openerp.mail = function (session) { var attach = this.attachment_ids[l]; if (!attach.formating) { attach.url = mail.ChatterUtils.get_attachment_url(this.session, this.id, attach.id); - attach.filetype = mail.ChatterUtils.filetype(attach.filename); + attach.filetype = mail.ChatterUtils.filetype(attach.filename || attach.name); attach.name = mail.ChatterUtils.breakword(attach.name || attach.filename); attach.formating = true; } @@ -1605,7 +1606,8 @@ openerp.mail = function (session) { this.node.params = _.extend({ 'display_indented_thread': -1, 'show_reply_button': false, - 'show_read_unread_button': false, + 'show_read_unread_button': true, + 'read_action': 'unread', 'show_record_name': false, 'show_compact_message': 1, }, this.node.params); diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index 9f88116acc7..03351c24ac2 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -216,9 +216,12 @@ openerp_mail_followers = function(session, mail) { display_subtypes:function (data) { var self = this; var subtype_list_ul = this.$('.oe_subtype_list'); - subtype_list_ul.empty(); - var records = data[this.view.datarecord.id || this.view.dataset.ids[0]].message_subtype_data; + var records = []; var nb_subtype = 0; + subtype_list_ul.empty(); + if (this.view.datarecord.id) { + records = data[this.view.datarecord.id].message_subtype_data; + } _(records).each(function (record) {nb_subtype++;}); if (nb_subtype > 1) { this.$('hr').show(); diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 63f8a10658f..4693adeb1ee 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -246,7 +246,7 @@ - + diff --git a/addons/membership/wizard/membership_invoice.py b/addons/membership/wizard/membership_invoice.py index 85a9cc0c85b..2425cd648ea 100644 --- a/addons/membership/wizard/membership_invoice.py +++ b/addons/membership/wizard/membership_invoice.py @@ -52,9 +52,16 @@ class membership_invoice(osv.osv_memory): 'amount': data.member_price } invoice_list = partner_obj.create_membership_invoice(cr, uid, context.get('active_ids', []), datas=datas, context=context) - - res = mod_obj.get_object_reference(cr, uid, 'account', 'view_account_invoice_filter') - + + try: + search_view_id = mod_obj.get_object_reference(cr, uid, 'account', 'view_account_invoice_filter')[1] + except ValueError: + search_view_id = False + try: + form_view_id = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')[1] + except ValueError: + form_view_id = False + return { 'domain': [('id', 'in', invoice_list)], 'name': 'Membership Invoices', @@ -62,7 +69,8 @@ class membership_invoice(osv.osv_memory): 'view_mode': 'tree,form', 'res_model': 'account.invoice', 'type': 'ir.actions.act_window', - 'search_view_id': res and res[1] or False + 'views': [(False, 'tree'), (form_view_id, 'form')], + 'search_view_id': search_view_id, } membership_invoice() diff --git a/addons/mrp/__openerp__.py b/addons/mrp/__openerp__.py index 66806c4f350..6e025ae2d90 100644 --- a/addons/mrp/__openerp__.py +++ b/addons/mrp/__openerp__.py @@ -28,7 +28,7 @@ 'category': 'Manufacturing', 'sequence': 18, 'summary': 'Manufacturing Orders, Bill of Materials, Routing', - 'images': ['images/bill_of_materials.jpeg', 'images/manufacturing_order.jpeg', 'images/planning_manufacturing_order.jpeg', 'images/production_analysis.jpeg', 'images/production_dashboard.jpeg','images/routings.jpeg','images/work_centers.jpeg'], + 'images': ['images/bill_of_materials.jpeg', 'images/manufacturing_order.jpeg', 'images/planning_manufacturing_order.jpeg', 'images/manufacturing_analysis.jpeg', 'images/production_dashboard.jpeg','images/routings.jpeg','images/work_centers.jpeg'], 'depends': ['product','procurement', 'stock', 'resource', 'purchase','process'], 'description': """ Manage the Manufacturing process in OpenERP diff --git a/addons/note/__openerp__.py b/addons/note/__openerp__.py index 797270637e2..2653c558923 100644 --- a/addons/note/__openerp__.py +++ b/addons/note/__openerp__.py @@ -57,6 +57,11 @@ Notes can be found in the 'Home' menu. 'css': [ 'static/src/css/note.css', ], + 'images': [ + 'images/note_kanban.jpeg', + 'images/note.jpeg', + 'images/categories_tree.jpeg' + ], 'installable': True, 'application': True, 'auto_install': False, diff --git a/addons/note/note.py b/addons/note/note.py index 7de91e4703a..2d95b15107a 100644 --- a/addons/note/note.py +++ b/addons/note/note.py @@ -29,7 +29,7 @@ class note_stage(osv.osv): _columns = { 'name': fields.char('Stage Name', translate=True, required=True), 'sequence': fields.integer('Sequence', help="Used to order the note stages"), - 'user_id': fields.many2one('res.users', 'Owner', help="Owner of the note stage.", required=True), + 'user_id': fields.many2one('res.users', 'Owner', help="Owner of the note stage.", required=True, ondelete='cascade'), 'fold': fields.boolean('Folded by Default'), } _order = 'sequence asc' @@ -112,7 +112,7 @@ class note_note(osv.osv): 'date_done': fields.date('Date done'), 'color': fields.integer('Color Index'), 'tag_ids' : fields.many2many('note.tag','note_tags_rel','note_id','tag_id','Tags'), - 'current_partner_id' : fields.function(_get_my_current_partner), + 'current_partner_id' : fields.function(_get_my_current_partner, type="many2one", relation='res.partner', string="Owner"), } _defaults = { 'open' : 1, diff --git a/addons/pad/pad.py b/addons/pad/pad.py index 12fa8254d00..90f5354054c 100644 --- a/addons/pad/pad.py +++ b/addons/pad/pad.py @@ -24,7 +24,7 @@ class pad_common(osv.osv_memory): # make sure pad server in the form of http://hostname if not pad["server"]: - return '' + return pad if not pad["server"].startswith('http'): pad["server"] = 'http://' + pad["server"] pad["server"] = pad["server"].rstrip('/') @@ -96,7 +96,7 @@ class pad_common(osv.osv_memory): field = v.column if hasattr(field,'pad_content_field'): pad = self.pad_generate_url(cr, uid, context) - default[k] = pad['url'] + default[k] = pad.get('url') return super(pad_common, self).copy(cr, uid, id, default, context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py index d68bd240c4f..cb05cef46c3 100644 --- a/addons/point_of_sale/__openerp__.py +++ b/addons/point_of_sale/__openerp__.py @@ -48,7 +48,7 @@ Main Features * Refund previous sales """, 'author': 'OpenERP SA', - 'images': ['images/cash_registers.jpeg', 'images/pos_analysis.jpeg','images/register_analysis.jpeg','images/sale_order_pos.jpeg','images/product_pos.jpeg'], + 'images': ['images/pos_touch_screen.jpeg', 'images/pos_session.jpeg', 'images/pos_analysis.jpeg','images/sale_order_pos.jpeg','images/product_pos.jpeg'], 'depends': ['sale_stock'], 'data': [ 'security/point_of_sale_security.xml', diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 970eb1fbed7..8e47d83aa24 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -853,8 +853,7 @@ class pos_order(osv.osv): inv_line['price_unit'] = line.price_unit inv_line['discount'] = line.discount inv_line['name'] = inv_name - inv_line['invoice_line_tax_id'] = ('invoice_line_tax_id' in inv_line)\ - and [(6, 0, inv_line['invoice_line_tax_id'])] or [] + inv_line['invoice_line_tax_id'] = [(6, 0, [x.id for x in line.product_id.taxes_id] )] inv_line_ref.create(cr, uid, inv_line, context=context) inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context) wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr) @@ -1156,7 +1155,6 @@ class pos_order_line(osv.osv): prod = self.pool.get('product.product').browse(cr, uid, product, context=context) - taxes = prod.taxes_id price = price_unit * (1 - (discount or 0.0) / 100.0) taxes = account_tax_obj.compute_all(cr, uid, prod.taxes_id, price, qty, product=prod, partner=False) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 2c086032ec3..d9a4d6d00a9 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -233,7 +233,9 @@ font-style: italic; cursor:pointer; } - +.point-of-sale .oe_pos_synch-notification.oe_inactive{ + cursor: default; +} .point-of-sale .oe_pos_synch-notification .oe_status_red{ display:inline-block; cursor:pointer; @@ -542,7 +544,9 @@ background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); - background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); + /* for some reason the -90deg orientation doesn't match the -webkit-linear-gradient. It should be 180deg here. + * webkit also insists on rendering *both* gradients instead of only the native one. So it doesn't looks right. ugh. + background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); */ /*background:#FFF;*/ padding: 3px; padding-top: 15px; @@ -607,7 +611,9 @@ background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); + /* troublesome in latest webkit background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1)); + */ /*background:#FFF;*/ padding: 3px; padding-top:15px; @@ -991,12 +997,20 @@ margin-bottom:10px; } .point-of-sale .order .summary .line{ + float: right; margin-right:15px; + margin-left: 15px; padding-top:5px; border-top: solid 2px; border-color:#777; } +.point-of-sale .order .summary .line .subentry{ + font-size: 10px; + font-weight: normal; + text-align: center; +} .point-of-sale .order .summary .line.empty{ + text-align: right; border-color:#BBB; color:#999; } diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 666c61919da..2be58d6ce97 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -40,6 +40,10 @@ function openerp_pos_db(instance, module){ //cache the data in memory to avoid roundtrips to the localstorage this.cache = {}; + this.product_by_id = {}; + this.product_by_ean13 = {}; + this.product_by_category_id = {}; + this.category_by_id = {}; this.root_category_id = 0; this.category_products = {}; @@ -49,6 +53,7 @@ function openerp_pos_db(instance, module){ this.category_search_string = {}; this.packagings_by_id = {}; this.packagings_by_product_id = {}; + this.packagings_by_ean13 = {}; }, /* returns the category object from its id. If you pass a list of id as parameters, you get * a list of category objects. @@ -137,7 +142,6 @@ function openerp_pos_db(instance, module){ /* saves a record store to the database */ save: function(store,data){ var str_data = JSON.stringify(data); - console.log('Storing '+ Math.round(str_data.length/1024.0)+' KB of data to store: '+store); localStorage[this.name + '_' + store] = JSON.stringify(data); this.cache[store] = data; }, @@ -153,8 +157,7 @@ function openerp_pos_db(instance, module){ return str + '\n'; }, add_products: function(products){ - var stored_products = this.load('products',{}); - var stored_categories = this.load('categories',{}); + var stored_categories = this.product_by_category_id; if(!products instanceof Array){ products = [products]; @@ -187,10 +190,11 @@ function openerp_pos_db(instance, module){ } this.category_search_string[ancestor] += search_string; } - stored_products[product.id] = product; + this.product_by_id[product.id] = product; + if(product.ean13){ + this.product_by_ean13[product.ean13] = product; + } } - this.save('products',stored_products); - this.save('categories',stored_categories); }, add_packagings: function(packagings){ for(var i = 0, len = packagings.length; i < len; i++){ @@ -200,6 +204,9 @@ function openerp_pos_db(instance, module){ this.packagings_by_product_id[pack.product_id[0]] = []; } this.packagings_by_product_id[pack.product_id[0]].push(pack); + if(pack.ean13){ + this.packagings_by_ean13[pack.ean13] = pack; + } } }, /* removes all the data from the database. TODO : being able to selectively remove data */ @@ -219,31 +226,24 @@ function openerp_pos_db(instance, module){ return count; }, get_product_by_id: function(id){ - return this.load('products',{})[id]; + return this.product_by_id[id]; }, get_product_by_ean13: function(ean13){ - var products = this.load('products',{}); - for(var i in products){ - if( products[i] && products[i].ean13 === ean13){ - return products[i]; - } + if(this.product_by_ean13[ean13]){ + return this.product_by_ean13[ean13]; } - for(var p in this.packagings_by_id){ - var pack = this.packagings_by_id[p]; - if( pack.ean === ean13){ - return products[pack.product_id[0]]; - } + var pack = this.packagings_by_ean13[ean13]; + if(pack){ + return this.product_by_id[pack.product_id[0]]; } return undefined; }, get_product_by_category: function(category_id){ - var stored_categories = this.load('categories',{}); - var stored_products = this.load('products',{}); - var product_ids = stored_categories[category_id]; + var product_ids = this.product_by_category_id[category_id]; var list = []; if (product_ids) { for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) { - list.push(stored_products[product_ids[i]]); + list.push(this.product_by_id[product_ids[i]]); } } return list; @@ -275,12 +275,9 @@ function openerp_pos_db(instance, module){ }, remove_order: function(order_id){ var orders = this.load('orders',[]); - console.log('Remove order:',order_id); - console.log('Order count:',orders.length); orders = _.filter(orders, function(order){ return order.id !== order_id; }); - console.log('Order count:',orders.length); this.save('orders',orders); }, get_orders: function(){ diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 556ea482aad..d2cf15b54f3 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,6 +1,24 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sale var QWeb = instance.web.qweb; + // rounds a value with a fixed number of decimals. + // round(3.141492,2) -> 3.14 + function round(value,decimals){ + var mult = Math.pow(10,decimals || 0); + return Math.round(value*mult)/mult; + } + window.round = round; + + // rounds a value with decimal form precision + // round(3.141592,0.025) ->3.125 + function round_pr(value,precision){ + if(!precision || precision < 0){ + throw new Error('round_pr(): needs a precision greater than zero, got '+precision+' instead'); + } + return Math.round(value / precision) * precision; + } + window.round_pr = round_pr; + // The PosModel contains the Point Of Sale's representation of the backend. // Since the PoS must work in standalone ( Without connection to the server ) @@ -24,8 +42,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy this.db = new module.PosLS(); // a database used to store the products and categories this.db.clear('products','categories'); - this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode - + this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode // default attributes values. If null, it will be loaded below. this.set({ @@ -101,8 +118,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }).then(function(company_partners){ self.get('company').contact_address = company_partners[0].contact_address; - return self.fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]); + return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('company').currency_id[0]]]); }).then(function(currencies){ + console.log('Currency:',currencies[0]); self.set('currency',currencies[0]); return self.fetch('product.uom', null, null); @@ -117,7 +135,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return self.fetch('product.packaging', null, null); }).then(function(packagings){ self.set('product.packaging',packagings); - + return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); }).then(function(users){ self.set('user_list',users); @@ -211,7 +229,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // logs the usefull posmodel data to the console for debug purposes log_loaded_data: function(){ console.log('PosModel data has been loaded:'); - console.log('PosModel: categories:',this.get('categories')); console.log('PosModel: units:',this.get('units')); console.log('PosModel: bank_statements:',this.get('bank_statements')); console.log('PosModel: journals:',this.get('journals')); @@ -339,19 +356,26 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.product = options.product; this.price = options.product.get('price'); this.quantity = 1; + this.quantityStr = '1'; this.discount = 0; + this.discountStr = '0'; this.type = 'unit'; this.selected = false; }, // sets a discount [0,100]% set_discount: function(discount){ - this.discount = Math.max(0,Math.min(100,discount)); + var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); + this.discount = disc; + this.discountStr = '' + disc; this.trigger('change'); }, // returns the discount [0,100]% get_discount: function(){ return this.discount; }, + get_discount_str: function(){ + return this.discountStr; + }, get_product_type: function(){ return this.type; }, @@ -359,13 +383,18 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // product's unity of measure properties. Quantities greater than zero will not get // rounded to zero set_quantity: function(quantity){ - if(_.isNaN(quantity)){ + if(quantity === 'remove'){ this.order.removeOrderline(this); - }else if(quantity !== undefined){ - this.quantity = Math.max(0,quantity); + return; + }else{ + var quant = Math.max(parseFloat(quantity) || 0, 0); var unit = this.get_unit(); - if(unit && this.quantity > 0 ){ - this.quantity = Math.max(unit.rounding, Math.round(quantity / unit.rounding) * unit.rounding); + if(unit){ + this.quantity = Math.max(unit.rounding, Math.round(quant / unit.rounding) * unit.rounding); + this.quantityStr = this.quantity.toFixed(Math.max(0,Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)))); + }else{ + this.quantity = quant; + this.quantityStr = '' + this.quantity; } } this.trigger('change'); @@ -374,6 +403,17 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal get_quantity: function(){ return this.quantity; }, + get_quantity_str: function(){ + return this.quantityStr; + }, + get_quantity_str_with_unit: function(){ + var unit = this.get_unit(); + if(unit && unit.name !== 'Unit(s)'){ + return this.quantityStr + ' ' + unit.name; + }else{ + return this.quantityStr; + } + }, // return the unit of measure of the product get_unit: function(){ var unit_id = (this.product.get('uos_id') || this.product.get('uom_id')); @@ -390,15 +430,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal get_product: function(){ return this.product; }, - // return the base price of this product (for this orderline) - get_price: function(){ - return this.price; - }, - // changes the base price of the product for this orderline - set_price: function(price){ - this.price = price; - this.trigger('change'); - }, // selects or deselects this orderline set_selected: function(selected){ this.selected = selected; @@ -429,7 +460,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal export_as_JSON: function() { return { qty: this.get_quantity(), - price_unit: this.get_price(), + price_unit: this.get_unit_price(), discount: this.get_discount(), product_id: this.get_product().get('id'), }; @@ -439,9 +470,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return { quantity: this.get_quantity(), unit_name: this.get_unit().name, - price: this.get_price(), + price: this.get_unit_price(), discount: this.get_discount(), product_name: this.get_product().get('name'), + price_display : this.get_display_price(), price_with_tax : this.get_price_with_tax(), price_without_tax: this.get_price_without_tax(), tax: this.get_tax(), @@ -449,6 +481,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal product_description_sale: this.get_product().get('description_sale'), }; }, + // changes the base price of the product for this orderline + set_unit_price: function(price){ + this.price = round(parseFloat(price) || 0, 2); + this.trigger('change'); + }, + get_unit_price: function(){ + var rounding = this.pos.get('currency').rounding; + return round_pr(this.price,rounding); + }, + get_display_price: function(){ + var rounding = this.pos.get('currency').rounding; + return round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding); + }, get_price_without_tax: function(){ return this.get_all_prices().priceWithoutTax; }, @@ -458,9 +503,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal get_tax: function(){ return this.get_all_prices().tax; }, - get_all_prices: function() { + get_all_prices: function(){ var self = this; - var base = this.get_quantity() * this.price * (1 - (this.get_discount() / 100)); + var currency_rounding = this.pos.get('currency').rounding; + var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding); var totalTax = base; var totalNoTax = base; @@ -474,12 +520,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal if (tax.price_include) { var tmp; if (tax.type === "percent") { - tmp = base - (base / (1 + tax.amount)); + tmp = base - round_pr(base / (1 + tax.amount),currency_rounding); } else if (tax.type === "fixed") { - tmp = tax.amount * self.get_quantity(); + tmp = round_pr(tax.amount * self.get_quantity(),currency_rounding); } else { throw "This type of tax is not supported by the point of sale: " + tax.type; } + tmp = round_pr(tmp,currency_rounding); taxtotal += tmp; totalNoTax -= tmp; } else { @@ -491,6 +538,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } else { throw "This type of tax is not supported by the point of sale: " + tax.type; } + tmp = round_pr(tmp,currency_rounding); taxtotal += tmp; totalTax += tmp; } @@ -515,7 +563,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, //sets the amount of money on this payment line set_amount: function(value){ - this.amount = value; + this.amount = parseFloat(value) || 0; this.trigger('change'); }, // returns the amount of money on this paymentline @@ -584,7 +632,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal line.set_quantity(options.quantity); } if(options.price !== undefined){ - line.set_price(options.price); + line.set_unit_price(options.price); } var last_orderline = this.getLastOrderline(); @@ -613,14 +661,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal getName: function() { return this.get('name'); }, - getTotal: function() { + getSubtotal : function(){ + return (this.get('orderLines')).reduce((function(sum, orderLine){ + return sum + orderLine.get_display_price(); + }), 0); + }, + getTotalTaxIncluded: function() { return (this.get('orderLines')).reduce((function(sum, orderLine) { return sum + orderLine.get_price_with_tax(); }), 0); }, getDiscountTotal: function() { return (this.get('orderLines')).reduce((function(sum, orderLine) { - return sum + (orderLine.get_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); + return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); }), 0); }, getTotalTaxExcluded: function() { @@ -639,10 +692,10 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }), 0); }, getChange: function() { - return this.getPaidTotal() - this.getTotal(); + return this.getPaidTotal() - this.getTotalTaxIncluded(); }, getDueLeft: function() { - return this.getTotal() - this.getPaidTotal(); + return this.getTotalTaxIncluded() - this.getPaidTotal(); }, // sets the type of receipt 'receipt'(default) or 'invoice' set_receipt_type: function(type){ @@ -698,10 +751,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return { orderlines: orderlines, paymentlines: paymentlines, - total_with_tax: this.getTotal(), + subtotal: this.getSubtotal(), + total_with_tax: this.getTotalTaxIncluded(), total_without_tax: this.getTotalTaxExcluded(), total_tax: this.getTax(), total_paid: this.getPaidTotal(), + total_discount: this.getDiscountTotal(), change: this.getChange(), name : this.getName(), client: client ? client.name : null , @@ -743,7 +798,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return { name: this.getName(), amount_paid: this.getPaidTotal(), - amount_total: this.getTotal(), + amount_total: this.getTotalTaxIncluded(), amount_tax: this.getTax(), amount_return: this.getChange(), lines: orderLines, @@ -800,20 +855,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal buffer: (this.get('buffer')) + newChar }); } - this.updateTarget(); + this.trigger('set_value',this.get('buffer')); }, deleteLastChar: function() { - var tempNewBuffer = this.get('buffer').slice(0, -1); - - if(!tempNewBuffer){ - this.set({ buffer: "0" }); - this.killTarget(); - }else{ - if (isNaN(tempNewBuffer)) { - tempNewBuffer = "0"; + if(this.get('buffer') === ""){ + if(this.get('mode') === 'quantity'){ + this.trigger('set_value','remove'); + }else{ + this.trigger('set_value',this.get('buffer')); } - this.set({ buffer: tempNewBuffer }); - this.updateTarget(); + }else{ + var newBuffer = this.get('buffer').slice(0,-1) || ""; + this.set({ buffer: newBuffer }); + this.trigger('set_value',this.get('buffer')); } }, switchSign: function() { @@ -822,7 +876,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.set({ buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer }); - this.updateTarget(); + this.trigger('set_value',this.get('buffer')); }, changeMode: function(newMode) { this.set({ @@ -836,15 +890,8 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal mode: "quantity" }); }, - updateTarget: function() { - var bufferContent, params; - bufferContent = this.get('buffer'); - if (bufferContent && !isNaN(bufferContent)) { - this.trigger('set_value', parseFloat(bufferContent)); - } - }, - killTarget: function(){ - this.trigger('set_value',Number.NaN); + resetValue: function(){ + this.set({buffer:'0'}); }, }); } diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index f8e3382c242..eca4a3c5915 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -368,7 +368,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({ template:'ChooseReceiptPopupWidget', show: function(){ - console.log('show'); this._super(); this.renderElement(); var self = this; @@ -603,7 +602,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // initiates the connection to the payment terminal and starts the update requests this.start = function(){ var def = new $.Deferred(); - console.log("START"); self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft()) .done(function(ack){ if(ack === 'ok'){ @@ -613,7 +611,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }else{ console.error('unknown payment request return value:',ack); } - console.log("START_END"); def.resolve(); }); return def; @@ -621,10 +618,8 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // gets updated status from the payment terminal and performs the appropriate consequences this.update = function(){ - console.log("UPDATE"); var def = new $.Deferred(); if(self.canceled){ - console.log("UPDATE_END"); return def.resolve(); } self.pos.proxy.payment_status() @@ -656,7 +651,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }else{ console.error('unknown status value:',status.status); } - console.log("UPDATE_END"); def.resolve(); }); return def; @@ -664,14 +658,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa // cancels a payment. this.cancel = function(){ - console.log("CANCEL"); if(!self.paid && !self.canceled){ self.canceled = true; self.pos.proxy.payment_cancel(); self.pos_widget.screen_selector.set_current_screen(self.previous_screen); self.queue.clear(); } - console.log("CANCEL_END"); return (new $.Deferred()).resolve(); } @@ -865,6 +857,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.bindPaymentLineEvents(); this.bind_orderline_events(); this.paymentlinewidgets = []; + this.focusedLine = null; }, show: function(){ this._super(); @@ -894,6 +887,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); this.updatePaymentSummary(); + this.line_refocus(); }, close: function(){ this._super(); @@ -931,17 +925,30 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.bind_orderline_events(); this.renderElement(); }, + line_refocus: function(lineWidget){ + if(lineWidget){ + if(this.focusedLine !== lineWidget){ + this.focusedLine = lineWidget; + } + } + if(this.focusedLine){ + this.focusedLine.focus(); + } + }, addPaymentLine: function(newPaymentLine) { var self = this; - var l = new module.PaymentlineWidget(null, { - payment_line: newPaymentLine + var l = new module.PaymentlineWidget(this, { + payment_line: newPaymentLine, }); l.on('delete_payment_line', self, function(r) { self.deleteLine(r); }); l.appendTo(this.$('#paymentlines')); this.paymentlinewidgets.push(l); - this.$('.paymentline-amount input:last').focus(); + if(this.numpadState){ + this.numpadState.resetValue(); + } + this.line_refocus(l); }, renderElement: function() { this._super(); @@ -958,25 +965,26 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, deleteLine: function(lineWidget) { this.currentPaymentLines.remove([lineWidget.payment_line]); + lineWidget.destroy(); }, updatePaymentSummary: function() { var currentOrder = this.pos.get('selectedOrder'); var paidTotal = currentOrder.getPaidTotal(); - var dueTotal = currentOrder.getTotal(); + var dueTotal = currentOrder.getTotalTaxIncluded(); var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; - this.$('#payment-due-total').html(dueTotal.toFixed(2)); - this.$('#payment-paid-total').html(paidTotal.toFixed(2)); - this.$('#payment-remaining').html(remaining.toFixed(2)); - this.$('#payment-change').html(change.toFixed(2)); - if((currentOrder.selected_orderline == undefined)) - remaining = 1 + this.$('#payment-due-total').html(this.format_currency(dueTotal)); + this.$('#payment-paid-total').html(this.format_currency(paidTotal)); + this.$('#payment-remaining').html(this.format_currency(remaining)); + this.$('#payment-change').html(this.format_currency(change)); + if(currentOrder.selected_orderline === undefined){ + remaining = 1; // What is this ? + } if(this.pos_widget.action_bar){ this.pos_widget.action_bar.set_button_disabled('validation', remaining > 0); } - this.$('.paymentline-amount input:last').focus(); }, set_numpad_state: function(numpadState) { if (this.numpadState) { @@ -998,5 +1006,4 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.currentPaymentLines.last().set_amount(val); }, }); - } diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js index 0aa0a44e7e7..124be4e9f82 100644 --- a/addons/point_of_sale/static/src/js/widget_base.js +++ b/addons/point_of_sale/static/src/js/widget_base.js @@ -22,14 +22,20 @@ function openerp_pos_basewidget(instance, module){ //module is instance.point_of if(this.pos && this.pos.get('currency')){ this.currency = this.pos.get('currency'); }else{ - this.currency = {symbol: '$', position: 'after'}; + this.currency = {symbol: '$', position: 'after', rounding: 0.01}; } + var decimals = Math.max(0,Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10))); + this.format_currency = function(amount){ + if(typeof amount === 'number'){ + amount = Math.round(amount*100)/100; + amount = amount.toFixed(decimals); + } if(this.currency.position === 'after'){ - return Math.round(amount*100)/100 + ' ' + this.currency.symbol; + return amount + ' ' + this.currency.symbol; }else{ - return this.currency.symbol + ' ' + Math.round(amount*100)/100; + return this.currency.symbol + ' ' + amount; } } diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 39ac2529a76..874c387ea23 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -186,7 +186,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }else if( mode === 'discount'){ order.getSelectedLine().set_discount(val); }else if( mode === 'price'){ - order.getSelectedLine().set_price(val); + order.getSelectedLine().set_unit_price(val); } } else { this.pos.get('selectedOrder').destroy(); @@ -269,8 +269,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, update_summary: function(){ var order = this.pos.get('selectedOrder'); - var total = order ? order.getTotal() : 0; - this.$('.summary .value.total').html(this.format_currency(total)); + var total = order ? order.getTotalTaxIncluded() : 0; + var taxes = order ? total - order.getTotalTaxExcluded() : 0; + this.$('.summary .total > .value').html(this.format_currency(total)); + this.$('.summary .total .subentry .value').html(this.format_currency(taxes)); }, set_display_mode: function(mode){ if(this.display_mode !== mode){ @@ -311,24 +313,34 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, changeAmount: function(event) { var newAmount = event.currentTarget.value; - if (newAmount && !isNaN(newAmount)) { - this.amount = parseFloat(newAmount); - this.payment_line.set_amount(this.amount); + var amount = parseFloat(newAmount); + if(!isNaN(amount)){ + this.amount = amount; + this.payment_line.set_amount(amount); } }, changedAmount: function() { - if (this.amount !== this.payment_line.get_amount()) + if (this.amount !== this.payment_line.get_amount()){ this.renderElement(); + } }, renderElement: function() { var self = this; this.name = this.payment_line.get_cashregister().get('journal_id')[1]; this._super(); - this.$('input').keyup(_.bind(this.changeAmount, this)); + this.$('input').keyup(function(event){ + self.changeAmount(event); + }); this.$('.delete-payment-line').click(function() { self.trigger('delete_payment_line', self); }); }, + focus: function(){ + var val = this.$('input')[0].value; + this.$('input')[0].focus(); + this.$('input')[0].value = val; + this.$('input')[0].select(); + }, }); module.OrderButtonWidget = module.PosBaseWidget.extend({ @@ -606,20 +618,15 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa if(this.scrollbar){ this.scrollbar.destroy(); } - - this.pos.get('products') - .chain() - .map(function(product) { - var product = new module.ProductWidget(self, { - model: product, - weight: self.weight, - click_product_action: self.click_product_action, - }) - self.productwidgets.push(product); - return product; - }) - .invoke('appendTo', this.$('.product-list')); - + var products = this.pos.get('products').models || []; + for(var i = 0, len = products.length; i < len; i++){ + var product = new module.ProductWidget(self, { + model: products[i], + click_product_action: this.click_product_action, + }); + this.productwidgets.push(product); + product.appendTo(this.$('.product-list')); + } this.scrollbar = new module.ScrollbarWidget(this,{ target_widget: this, target_selector: '.product-list-scroller', diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 41a061e78b4..3da76ec12d8 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -48,11 +48,17 @@ -

- -
-
-
+ +
+ +
+
+
+ +
+
+
+
@@ -214,11 +220,7 @@ Total: - - - - - +
@@ -227,31 +229,19 @@ Paid: - - - - - +
Remaining: - - - - - +
Change: - - - - - +
@@ -431,9 +421,13 @@
- - Total: 0.00 € - +
+
+ Total: 0.00 € +
Taxes: 0.00€
+
+
+
@@ -500,26 +494,26 @@ - +
    - +
  • - + at - + /
  • - +
  • With a - % + % discount
  • @@ -571,33 +565,36 @@ Shop:

    - +
    - - + +
    - With a % discount + With a % discount
    - + - +

    +
    Subtotal: + +
    Tax: - +
    Discount: - +
    Total: - +

    @@ -607,14 +604,14 @@ - +
    Change: - +
    diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index 50efe0f500e..1ccc08236c7 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -103,7 +103,7 @@ class procurement_order(osv.osv): readonly=True, required=True, help="If you encode manually a Procurement, you probably want to use" \ " a make to order method."), 'note': fields.text('Note'), - 'message': fields.char('Latest error', size=124, help="Exception occurred while computing procurement orders."), + 'message': fields.char('Latest error', help="Exception occurred while computing procurement orders."), 'state': fields.selection([ ('draft','Draft'), ('cancel','Cancelled'), @@ -367,10 +367,22 @@ class procurement_order(osv.osv): if message: message = _("Procurement '%s' is in exception: ") % (procurement.name) + message - cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id)) + #temporary context passed in write to prevent an infinite loop + ctx_wkf = dict(context or {}) + ctx_wkf['workflow.trg_write.%s' % self._name] = False + self.write(cr, uid, [procurement.id], {'message': message},context=ctx_wkf) self.message_post(cr, uid, [procurement.id], body=message, context=context) return ok + def _workflow_trigger(self, cr, uid, ids, trigger, context=None): + """ Don't trigger workflow for the element specified in trigger + """ + wkf_op_key = 'workflow.%s.%s' % (trigger, self._name) + if context and not context.get(wkf_op_key, True): + # make sure we don't have a trigger loop while processing triggers + return + return super(procurement_order,self)._workflow_trigger(cr, uid, ids, trigger, context=context) + def action_produce_assign_service(self, cr, uid, ids, context=None): """ Changes procurement state to Running. @return: True diff --git a/addons/project/__openerp__.py b/addons/project/__openerp__.py index 85e33be777a..c0a516cf555 100644 --- a/addons/project/__openerp__.py +++ b/addons/project/__openerp__.py @@ -33,7 +33,10 @@ 'images/project_task_tree.jpeg', 'images/project_task.jpeg', 'images/project.jpeg', - 'images/task_analysis.jpeg' + 'images/task_analysis.jpeg', + 'images/project_kanban.jpeg', + 'images/task_kanban.jpeg', + 'images/task_stages.jpeg' ], 'depends': [ 'base_setup', diff --git a/addons/project/project_data.xml b/addons/project/project_data.xml index 56a21342d9e..025327d70b3 100644 --- a/addons/project/project_data.xml +++ b/addons/project/project_data.xml @@ -94,16 +94,19 @@ Task Blocked project.task + Task blocked Task Done project.task + Task closed Stage Changed project.task + Stage changed diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index b0b7193b7aa..8eb7281cd24 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -501,6 +501,7 @@ class project_issue(base_stage, osv.osv): 'description': desc, 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), + 'partner_id': msg.get('author_id', False), 'user_id': False, } if msg.get('priority'): diff --git a/addons/project_issue/project_issue_data.xml b/addons/project_issue/project_issue_data.xml index 55ec90b430d..591e6c04878 100644 --- a/addons/project_issue/project_issue_data.xml +++ b/addons/project_issue/project_issue_data.xml @@ -59,16 +59,19 @@ Access all issues from the top Project menu, and access the issues of a specific Issue Blocked project.issue + Issue blocked Issue Closed project.issue + Issue closed Stage Changed project.issue + Stage changed diff --git a/addons/purchase/purchase_data.xml b/addons/purchase/purchase_data.xml index 72c3f5ab044..ceb43ba281a 100644 --- a/addons/purchase/purchase_data.xml +++ b/addons/purchase/purchase_data.xml @@ -52,10 +52,12 @@ RFQ Confirmed + purchase.order RFQ Approved + purchase.order diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py index 44733be3e39..a693fd11024 100644 --- a/addons/sale/res_config.py +++ b/addons/sale/res_config.py @@ -19,10 +19,14 @@ # ############################################################################## +import logging + from openerp.osv import fields, osv from openerp import pooler from openerp.tools.translate import _ +_logger = logging.getLogger(__name__) + class sale_configuration(osv.osv_memory): _inherit = 'sale.config.settings' @@ -81,8 +85,12 @@ Example: Product: this product is deprecated, do not purchase more than 5. user = self.pool.get('res.users').browse(cr, uid, uid, context) res['time_unit'] = user.company_id.project_time_mode_id.id else: - product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant') - res['time_unit'] = product.uom_id.id + try: + product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant') + res['time_unit'] = product.uom_id.id + except ValueError: + # keep default value in that case + _logger.warning("Product with xml_id 'product.product_product_consultant' not found") return res def _get_default_time_unit(self, cr, uid, context=None): @@ -98,16 +106,11 @@ Example: Product: this product is deprecated, do not purchase more than 5. wizard = self.browse(cr, uid, ids)[0] if wizard.time_unit: - product = False try: product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant') - except: - #product with xml_id product_product_consultant has not been found. Don't do anything except logging the exception - import logging - _logger = logging.getLogger(__name__) - _logger.warning("Warning, product with xml_id 'product_product_consultant' hasn't been found") - if product: product.write({'uom_id': wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id}) + except ValueError: + _logger.warning("Product with xml_id 'product.product_product_consultant' not found, UoMs not updated!") if wizard.module_project and wizard.time_unit: user = self.pool.get('res.users').browse(cr, uid, uid, context) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 66653c61849..cddde4a5f08 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -926,7 +926,6 @@ class sale_order_line(osv.osv): elif uom: # whether uos is set or not default_uom = product_obj.uom_id and product_obj.uom_id.id q = product_uom_obj._compute_qty(cr, uid, uom, qty, default_uom) - result['product_uom'] = default_uom if product_obj.uos_id: result['product_uos'] = product_obj.uos_id.id result['product_uos_qty'] = qty * product_obj.uos_coeff diff --git a/addons/sale/sale_data.xml b/addons/sale/sale_data.xml index 6d7cd043c39..a0e93cbf581 100644 --- a/addons/sale/sale_data.xml +++ b/addons/sale/sale_data.xml @@ -48,11 +48,13 @@ Quotation send sale.order + Quotation send Sales Order Confirmed sale.order + Quotation confirmed diff --git a/addons/sale/test/sale_order_demo.yml b/addons/sale/test/sale_order_demo.yml index 7e5a223e8b0..75e17ebeb02 100644 --- a/addons/sale/test/sale_order_demo.yml +++ b/addons/sale/test/sale_order_demo.yml @@ -14,3 +14,23 @@ !assert {model: sale.order, id: sale.sale_order_test1, string: The onchange function of product was not correctly triggered}: - order_line[0].name == u'[LCD17] 17\u201d LCD Monitor' - order_line[0].price_unit == 1350.0 + - order_line[0].product_uom_qty == 8 + - order_line[0].product_uom.id == ref('product.product_uom_unit') + +- + I create another sale order +- + !record {model: sale.order, id: sale_order_test2}: + partner_id: base.res_partner_2 + order_line: + - product_id: product.product_product_7 + product_uom_qty: 16 + product_uom: product.product_uom_dozen +- + I verify that the onchange was correctly triggered +- + !assert {model: sale.order, id: sale.sale_order_test2, string: The onchange function of product was not correctly triggered}: + - order_line[0].name == u'[LCD17] 17\u201d LCD Monitor' + - order_line[0].price_unit == 1350.0 * 12 + - order_line[0].product_uom.id == ref('product.product_uom_dozen') + - order_line[0].product_uom_qty == 16 \ No newline at end of file diff --git a/addons/stock/res_config_view.xml b/addons/stock/res_config_view.xml index 9cc8553f4ea..3de3104a970 100644 --- a/addons/stock/res_config_view.xml +++ b/addons/stock/res_config_view.xml @@ -91,7 +91,7 @@