From 70028097b9683696d9b26e5782df71b0414eeae3 Mon Sep 17 00:00:00 2001 From: "Ravish Murari (OpenERP)" Date: Mon, 16 Jul 2012 18:40:00 +0530 Subject: [PATCH 01/82] [FIX] recurring models for account not showing company lp bug: https://launchpad.net/bugs/999315 fixed bzr revid: rmu@tinyerp.com-20120716131000-tvwhhz4xcmwpubaj --- addons/account/account_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 80202d97d6c..9fd37ea94fe 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -1547,7 +1547,7 @@ - + @@ -1567,7 +1567,7 @@ - + From d397e599f546329ed659c4355f893e9a240fbee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 22 Aug 2012 15:55:46 +0200 Subject: [PATCH 02/82] [BRANCH] locadis: first commit : start of the branch bzr revid: fva@openerp.com-20120822135546-kwbo893th1pv2vbs --- addons/locadis/__init__.py | 25 + addons/locadis/__openerp__.py | 56 ++ addons/locadis/point_of_sale.py | 1303 +++++++++++++++++++++++++ addons/locadis/static/src/css/pos.css | 4 + addons/locadis/static/src/js/hello.js | 12 + addons/locadis/static/src/js/main.js | 24 + addons/locadis/static/src/xml/pos.xml | 15 + 7 files changed, 1439 insertions(+) create mode 100644 addons/locadis/__init__.py create mode 100644 addons/locadis/__openerp__.py create mode 100644 addons/locadis/point_of_sale.py create mode 100644 addons/locadis/static/src/css/pos.css create mode 100644 addons/locadis/static/src/js/hello.js create mode 100644 addons/locadis/static/src/js/main.js create mode 100644 addons/locadis/static/src/xml/pos.xml diff --git a/addons/locadis/__init__.py b/addons/locadis/__init__.py new file mode 100644 index 00000000000..e57b362135b --- /dev/null +++ b/addons/locadis/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +#import point_of_sale + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/locadis/__openerp__.py b/addons/locadis/__openerp__.py new file mode 100644 index 00000000000..056b8da7fcf --- /dev/null +++ b/addons/locadis/__openerp__.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Locadis Point Of Sale', + 'version': '1.0.1', + 'category': 'Point Of Sale', + "sequence": 6, + "summary": "Point of sale extensions for Locadis", + 'description': """ +FIXME: product description + """, + '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'], + 'depends': ['point_of_sale'], + 'init_xml': [], + 'update_xml': [ + ], + 'demo_xml': [ + ], + 'test': [ + ], + 'installable': True, + 'application': True, + # Web client + 'js': [ + 'static/src/js/main.js' + ], + 'css': [ + 'static/src/css/pos.css' + ], + 'qweb': [ + 'static/src/xml/pos.xml' + ], + 'auto_install': False, +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/locadis/point_of_sale.py b/addons/locadis/point_of_sale.py new file mode 100644 index 00000000000..f4d32985688 --- /dev/null +++ b/addons/locadis/point_of_sale.py @@ -0,0 +1,1303 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import pdb +import openerp +import addons +import openerp.addons.product.product + +import time +from datetime import datetime +from dateutil.relativedelta import relativedelta +import logging + +import netsvc +from osv import fields, osv +import tools +from tools.translate import _ +from decimal import Decimal +import decimal_precision as dp + +_logger = logging.getLogger(__name__) + +class pos_config(osv.osv): + _name = 'pos.config' + + POS_CONFIG_STATE = [ + ('active', 'Active'), + ('inactive', 'Inactive'), + ('deprecated', 'Deprecated') + ] + + _columns = { + 'name' : fields.char('Point of Sale Name', size=32, select=1, + required=True, help="An internal identification of the point of sale"), + 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', + 'pos_config_id', 'journal_id', 'Available Payment Methods', + domain="[('journal_user', '=', True )]",), + 'shop_id' : fields.many2one('sale.shop', 'Shop', + required=True), + 'journal_id' : fields.many2one('account.journal', 'Sale Journal', + domain=[('type', '=', 'sale')], + help="Accounting journal used to post sales entries."), + 'iface_self_checkout' : fields.boolean('Self Checkout Mode', + help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, OpenERP uses the normal cashier mode by default."), + 'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'), + 'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'), + 'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'), + 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'), + 'iface_print_via_proxy' : fields.boolean('Print via Proxy'), + + 'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True), + 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, + help="This sequence is automatically created by OpenERP but you can change it "\ + "to customize the reference numbers of your orders."), + 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), + 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), + } + + def name_get(self, cr, uid, ids, context=None): + result = [] + states = { + 'opening_control': _('Opening Control'), + 'opened': _('In Progress'), + 'closing_control': _('Closing Control'), + 'closed': _('Closed & Posted'), + } + for record in self.browse(cr, uid, ids, context=context): + if (not record.session_ids) or (record.session_ids[0].state=='closed'): + result.append((record.id, record.name+' ('+_('not used')+')')) + continue + session = record.session_ids[0] + result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) + return result + + + def _default_payment_journal(self, cr, uid, context=None): + res = self.pool.get('account.journal').search(cr, uid, [('type', 'in', ('bank','cash'))], limit=2) + return res or [] + + def _default_sale_journal(self, cr, uid, context=None): + res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1) + return res and res[0] or False + + def _default_shop(self, cr, uid, context=None): + res = self.pool.get('sale.shop').search(cr, uid, []) + return res and res[0] or False + + _defaults = { + 'state' : POS_CONFIG_STATE[0][0], + 'shop_id': _default_shop, + 'journal_id': _default_sale_journal, + 'journal_ids': _default_payment_journal, + 'group_by' : True, + } + + def set_active(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'active'}, context=context) + + def set_inactive(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) + + def set_deprecate(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) + + def create(self, cr, uid, values, context=None): + proxy = self.pool.get('ir.sequence') + sequence_values = dict( + name='PoS %s' % values['name'], + padding=5, + prefix="%s/" % values['name'], + ) + sequence_id = proxy.create(cr, uid, sequence_values, context=context) + values['sequence_id'] = sequence_id + return super(pos_config, self).create(cr, uid, values, context=context) + + def unlink(self, cr, uid, ids, context=None): + for obj in self.browse(cr, uid, ids, context=context): + if obj.sequence_id: + obj.sequence_id.unlink() + return super(pos_config, self).unlink(cr, uid, ids, context=context) + +class pos_session(osv.osv): + _name = 'pos.session' + _order = 'id desc' + + POS_SESSION_STATE = [ + ('opening_control', 'Opening Control'), # Signal open + ('opened', 'In Progress'), # Signal closing + ('closing_control', 'Closing Control'), # Signal close + ('closed', 'Closed & Posted'), + ] + + def _compute_cash_journal_id(self, cr, uid, ids, fieldnames, args, context=None): + result = dict.fromkeys(ids, False) + for record in self.browse(cr, uid, ids, context=context): + for st in record.statement_ids: + if st.journal_id.type == 'cash': + result[record.id] = st.journal_id.id + break + return result + + def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None): + result = dict.fromkeys(ids, False) + for record in self.browse(cr, uid, ids, context=context): + for st in record.statement_ids: + if st.journal_id.type == 'cash': + result[record.id] = st.id + break + return result + + def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None): + result = {} + + for record in self.browse(cr, uid, ids, context=context): + has_opening_control = False + has_closing_control = False + + for journal in record.config_id.journal_ids: + if journal.opening_control == True: + has_opening_control = True + if journal.closing_control == True: + has_closing_control = True + + if has_opening_control and has_closing_control: + break + + values = { + 'has_opening_control': has_opening_control, + 'has_closing_control': has_closing_control, + } + result[record.id] = values + + return result + + _columns = { + 'config_id' : fields.many2one('pos.config', 'Point of Sale', + help="The physical point of sale you will use.", + required=True, + select=1, + domain="[('state', '=', 'active')]", + ), + + 'name' : fields.char('Session ID', size=32, required=True, readonly=True), + 'user_id' : fields.many2one('res.users', 'Responsible', + required=True, + select=1, + readonly=True, + states={'opening_control' : [('readonly', False)]} + ), + 'start_at' : fields.datetime('Opening Date', readonly=True), + 'stop_at' : fields.datetime('Closing Date', readonly=True), + + 'state' : fields.selection(POS_SESSION_STATE, 'State', + required=True, readonly=True, + select=1), + + 'cash_journal_id' : fields.function(_compute_cash_journal_id, method=True, + type='many2one', relation='account.journal', + string='Cash Journal', store=True), + 'cash_register_id' : fields.function(_compute_cash_register_id, method=True, + type='many2one', relation='account.bank.statement', + string='Cash Register', store=True), + + 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids', + type='one2many', relation='account.cashbox.line', + string='Opening Cash Control'), + 'details_ids' : fields.related('cash_register_id', 'details_ids', + type='one2many', relation='account.cashbox.line', + string='Cash Control'), + + 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', + type='float', + digits_compute=dp.get_precision('Account'), + string="Ending Balance", + help="Computed using the cash control lines", + readonly=True), + 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', + type='float', + digits_compute=dp.get_precision('Account'), + string="Starting Balance", + help="Computed using the cash control at the opening.", + readonly=True), + 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', + string='Total Cash Transaction', + readonly=True), + 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', + type='float', + digits_compute=dp.get_precision('Account'), + string="Computed Balance", + help="Computed with the initial cash control and the sum of all payments.", + readonly=True), + 'cash_register_difference' : fields.related('cash_register_id', 'difference', + type='float', + string='Difference', + help="Difference between the counted cash control at the closing and the computed balance.", + readonly=True), + + 'journal_ids' : fields.related('config_id', 'journal_ids', + type='many2many', + readonly=True, + relation='account.journal', + string='Available Payment Methods'), + 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), + + 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), + 'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'), + 'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'), + } + + _defaults = { + 'name' : '/', + 'user_id' : lambda obj, cr, uid, context: uid, + 'state' : 'opening_control', + } + + _sql_constraints = [ + ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), + ] + + def _check_unicity(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=None): + # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user + domain = [ + ('state', 'not in', ('closed','closing_control')), + ('user_id', '=', uid) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True + + def _check_pos_config(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=None): + domain = [ + ('state', '!=', 'closed'), + ('config_id', '=', session.config_id.id) + ] + count = self.search_count(cr, uid, domain, context=context) + if count>1: + return False + return True + + _constraints = [ + (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']), + (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']), + ] + + def create(self, cr, uid, values, context=None): + config_id = values.get('config_id', False) or False + if config_id: + # journal_id is not required on the pos_config because it does not + # exists at the installation. If nothing is configured at the + # installation we do the minimal configuration. Impossible to do in + # the .xml files as the CoA is not yet installed. + jobj = self.pool.get('pos.config') + pos_config = jobj.browse(cr, uid, config_id, context=context) + if not pos_config.journal_id: + jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] + if jid: + jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context) + else: + raise osv.except_osv( _('error!'), + _("Unable to open the session. You have to assign a sale journal to your point of sale.")) + + # define some cash journal if no payment method exists + if not pos_config.journal_ids: + cashids = self.pool.get('account.journal').search(cr, uid, [('journal_user','=',True)], context=context) + if not cashids: + cashids = self.pool.get('account.journal').search(cr, uid, [('type','=','cash')], context=context) + self.pool.get('account.journal').write(cr, uid, cashids, {'journal_user': True}) + jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) + + + pos_config = jobj.browse(cr, uid, config_id, context=context) + bank_statement_ids = [] + for journal in pos_config.journal_ids: + bank_values = { + 'journal_id' : journal.id, + 'user_id' : uid, + } + statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context) + bank_statement_ids.append(statement_id) + + values.update({ + 'name' : pos_config.sequence_id._next(), + 'statement_ids' : [(6, 0, bank_statement_ids)], + 'config_id': config_id + }) + + return super(pos_session, self).create(cr, uid, values, context=context) + + def unlink(self, cr, uid, ids, context=None): + for obj in self.browse(cr, uid, ids, context=context): + for statement in obj.statement_ids: + statement.unlink(context=context) + return True + + def wkf_action_open(self, cr, uid, ids, context=None): + # second browse because we need to refetch the data from the DB for cash_register_id + for record in self.browse(cr, uid, ids, context=context): + values = {} + if not record.start_at: + values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S') + values['state'] = 'opened' + record.write(values, context=context) + for st in record.statement_ids: + st.button_open(context=context) + + return self.open_frontend_cb(cr, uid, ids, context=context) + + def wkf_action_opening_control(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context) + + def wkf_action_closing_control(self, cr, uid, ids, context=None): + for session in self.browse(cr, uid, ids, context=context): + for statement in session.statement_ids: + if statement.id <> session.cash_register_id.id: + if statement.balance_end<>statement.balance_end_real: + self.pool.get('account.bank.statement').write(cr, uid, + [statement.id], {'balance_end_real': statement.balance_end}) + return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) + + def wkf_action_close(self, cr, uid, ids, context=None): + # Close CashBox + bsl = self.pool.get('account.bank.statement.line') + for record in self.browse(cr, uid, ids, context=context): + for st in record.statement_ids: + if abs(st.difference) > st.journal_id.amount_authorized_diff: + # The pos manager can close statements with maximums. + if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"): + raise osv.except_osv( _('Error!'), + _("Your ending balance is too different from the theorical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff)) + if st.difference: + if st.difference > 0.0: + name= _('Point of Sale Profit') + account_id = st.journal_id.profit_account_id.id + else: + account_id = st.journal_id.loss_account_id.id + name= _('Point of Sale Loss') + if not account_id: + raise osv.except_osv( _('Error!'), + _("Please set your profit and loss accounts on your payment method '%s'. This will allow OpenERP to post the difference of %.2f in your ending balance. To close this session, you can update the 'Closing Cash Control' to avoid any difference.") % (st.journal_id.name,st.difference)) + bsl.create(cr, uid, { + 'statement_id': st.id, + 'amount': st.difference, + 'ref': record.name, + 'name': name, + 'account_id': account_id + }, context=context) + + getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context) + self._confirm_orders(cr, uid, ids, context=context) + self.write(cr, uid, ids, {'state' : 'closed'}, context=context) + + obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1] + return { + 'type' : 'ir.actions.client', + 'name' : 'Point of Sale Menu', + 'tag' : 'reload', + 'params' : {'menu_id': obj}, + } + + def _confirm_orders(self, cr, uid, ids, context=None): + wf_service = netsvc.LocalService("workflow") + + for session in self.browse(cr, uid, ids, context=context): + order_ids = [order.id for order in session.order_ids if order.state == 'paid'] + + move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context) + + self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context) + + for order in session.order_ids: + if order.state != 'paid': + raise osv.except_osv( + _('Error!'), + _("You cannot confirm all orders of this session, because they have not the 'paid' status")) + else: + wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr) + + return True + + def open_frontend_cb(self, cr, uid, ids, context=None): + if not context: + context = {} + if not ids: + return {} + context.update({'session_id' : ids[0]}) + return { + 'type' : 'ir.actions.client', + 'name' : 'Start Point Of Sale', + 'tag' : 'pos.ui', + 'context' : context, + } + +class pos_order(osv.osv): + _name = "pos.order" + _description = "Point of Sale" + _order = "id desc" + + def create_from_ui(self, cr, uid, orders, context=None): + #_logger.info("orders: %r", orders) + order_ids = [] + for tmp_order in orders: + order = tmp_order['data'] + order_id = self.create(cr, uid, { + 'name': order['name'], + 'user_id': order['user_id'] or False, + 'session_id': order['pos_session_id'], + 'lines': order['lines'] + }, context) + + for payments in order['statement_ids']: + payment = payments[2] + self.add_payment(cr, uid, order_id, { + 'amount': payment['amount'] or 0.0, + 'payment_date': payment['name'], + 'payment_name': payment.get('note', False), + 'journal': payment['journal_id'] + }, context=context) + + if order['amount_return']: + session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) + self.add_payment(cr, uid, order_id, { + 'amount': -order['amount_return'], + 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'), + 'payment_name': _('return'), + 'journal': session.cash_journal_id.id + }, context=context) + order_ids.append(order_id) + wf_service = netsvc.LocalService("workflow") + wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) + return order_ids + + def unlink(self, cr, uid, ids, context=None): + for rec in self.browse(cr, uid, ids, context=context): + if rec.state not in ('draft','cancel'): + raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.')) + return super(pos_order, self).unlink(cr, uid, ids, context=context) + + def onchange_partner_id(self, cr, uid, ids, part=False, context=None): + if not part: + return {'value': {}} + pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id + return {'value': {'pricelist_id': pricelist}} + + def _amount_all(self, cr, uid, ids, name, args, context=None): + tax_obj = self.pool.get('account.tax') + cur_obj = self.pool.get('res.currency') + res = {} + for order in self.browse(cr, uid, ids, context=context): + res[order.id] = { + 'amount_paid': 0.0, + 'amount_return':0.0, + 'amount_tax':0.0, + } + val1 = val2 = 0.0 + cur = order.pricelist_id.currency_id + for payment in order.statement_ids: + res[order.id]['amount_paid'] += payment.amount + res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0) + for line in order.lines: + val1 += line.price_subtotal_incl + val2 += line.price_subtotal + res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2) + res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1) + return res + + def copy(self, cr, uid, id, default=None, context=None): + if not default: + default = {} + d = { + 'state': 'draft', + 'invoice_id': False, + 'account_move': False, + 'picking_id': False, + 'statement_ids': [], + 'nb_print': 0, + 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'), + } + d.update(default) + return super(pos_order, self).copy(cr, uid, id, d, context=context) + + _columns = { + 'name': fields.char('Order Ref', size=64, required=True, readonly=True), + 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True), + 'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True), + 'date_order': fields.datetime('Order Date', readonly=True, select=True), + 'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It can be a reliever, a student or an interim employee."), + 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), + 'amount_total': fields.function(_amount_all, string='Total', multi='all'), + 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'), + 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), + 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True), + 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True), + 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True), + 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}), + + 'session_id' : fields.many2one('pos.session', 'Session', + #required=True, + select=1, + domain="[('state', '=', 'opened')]", + states={'draft' : [('readonly', False)]}, + readonly=True), + + 'state': fields.selection([('draft', 'New'), + ('cancel', 'Cancelled'), + ('paid', 'Paid'), + ('done', 'Posted'), + ('invoiced', 'Invoiced')], + 'Status', readonly=True), + + 'invoice_id': fields.many2one('account.invoice', 'Invoice'), + 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True), + 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True), + 'note': fields.text('Internal Notes'), + 'nb_print': fields.integer('Number of Print', readonly=True), + + 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True), + } + + def _default_session(self, cr, uid, context=None): + so = self.pool.get('pos.session') + session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context) + return session_ids and session_ids[0] or False + + def _default_pricelist(self, cr, uid, context=None): + res = self.pool.get('sale.shop').search(cr, uid, [], context=context) + if res: + shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context) + return shop.pricelist_id and shop.pricelist_id.id or False + return False + + _defaults = { + 'user_id': lambda self, cr, uid, context: uid, + 'state': 'draft', + 'name': '/', + 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), + 'nb_print': 0, + 'session_id': _default_session, + 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, + 'pricelist_id': _default_pricelist, + } + + def create(self, cr, uid, values, context=None): + values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order') + return super(pos_order, self).create(cr, uid, values, context=context) + + def test_paid(self, cr, uid, ids, context=None): + """A Point of Sale is paid when the sum + @return: True + """ + for order in self.browse(cr, uid, ids, context=context): + if order.lines and not order.amount_total: + return True + if (not order.lines) or (not order.statement_ids) or \ + (abs(order.amount_total-order.amount_paid) > 0.00001): + return False + return True + + def create_picking(self, cr, uid, ids, context=None): + """Create a picking for each order and validate it.""" + picking_obj = self.pool.get('stock.picking') + partner_obj = self.pool.get('res.partner') + move_obj = self.pool.get('stock.move') + + for order in self.browse(cr, uid, ids, context=context): + if not order.state=='draft': + continue + addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {} + picking_id = picking_obj.create(cr, uid, { + 'origin': order.name, + 'partner_id': addr.get('delivery',False), + 'type': 'out', + 'company_id': order.company_id.id, + 'move_type': 'direct', + 'note': order.note or "", + 'invoice_state': 'none', + 'auto_picking': True, + }, context=context) + self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context) + location_id = order.shop_id.warehouse_id.lot_stock_id.id + output_id = order.shop_id.warehouse_id.lot_output_id.id + + for line in order.lines: + if line.product_id and line.product_id.type == 'service': + continue + if line.qty < 0: + location_id, output_id = output_id, location_id + + move_obj.create(cr, uid, { + 'name': line.name, + 'product_uom': line.product_id.uom_id.id, + 'product_uos': line.product_id.uom_id.id, + 'picking_id': picking_id, + 'product_id': line.product_id.id, + 'product_uos_qty': abs(line.qty), + 'product_qty': abs(line.qty), + 'tracking_id': False, + 'state': 'draft', + 'location_id': location_id, + 'location_dest_id': output_id, + }, context=context) + if line.qty < 0: + location_id, output_id = output_id, location_id + + wf_service = netsvc.LocalService("workflow") + wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr) + picking_obj.force_assign(cr, uid, [picking_id], context) + return True + + def cancel_order(self, cr, uid, ids, context=None): + """ Changes order state to cancel + @return: True + """ + stock_picking_obj = self.pool.get('stock.picking') + for order in self.browse(cr, uid, ids, context=context): + wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr) + if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel': + raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.')) + self.write(cr, uid, ids, {'state': 'cancel'}, context=context) + return True + + def add_payment(self, cr, uid, order_id, data, context=None): + """Create a new payment for the order""" + if not context: + context = {} + statement_obj = self.pool.get('account.bank.statement') + statement_line_obj = self.pool.get('account.bank.statement.line') + prod_obj = self.pool.get('product.product') + property_obj = self.pool.get('ir.property') + curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id + curr_company = curr_c.id + order = self.browse(cr, uid, order_id, context=context) + args = { + 'amount': data['amount'], + } + if 'payment_date' in data: + args['date'] = data['payment_date'] + args['name'] = order.name + if data.get('payment_name', False): + args['name'] = args['name'] + ': ' + data['payment_name'] + account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context) + args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \ + and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False + args['partner_id'] = order.partner_id and order.partner_id.id or None + + if not args['account_id']: + if not args['partner_id']: + msg = _('There is no receivable account defined to make payment.') + else: + msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,) + raise osv.except_osv(_('Configuration Error!'), msg) + + context.pop('pos_session_id', False) + + try: + journal_id = long(data['journal']) + except Exception: + journal_id = False + + statement_id = False + for statement in order.session_id.statement_ids: + if statement.journal_id.id == journal_id: + statement_id = statement.id + break + + if not statement_id: + raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.')) + + args.update({ + 'statement_id' : statement_id, + 'pos_statement_id' : order_id, + 'journal_id' : journal_id, + 'type' : 'customer', + 'ref' : order.name, + }) + + statement_line_obj.create(cr, uid, args, context=context) + + wf_service = netsvc.LocalService("workflow") + wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) + wf_service.trg_write(uid, 'pos.order', order_id, cr) + + return statement_id + + def refund(self, cr, uid, ids, context=None): + """Create a copy of order for refund order""" + clone_list = [] + line_obj = self.pool.get('pos.order.line') + for order in self.browse(cr, uid, ids, context=context): + clone_id = self.copy(cr, uid, order.id, { + 'name': order.name + ' REFUND', + }, context=context) + clone_list.append(clone_id) + + for clone in self.browse(cr, uid, clone_list, context=context): + for order_line in clone.lines: + line_obj.write(cr, uid, [order_line.id], { + 'qty': -order_line.qty + }, context=context) + + new_order = ','.join(map(str,clone_list)) + abs = { + #'domain': "[('id', 'in', ["+new_order+"])]", + 'name': _('Return Products'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'pos.order', + 'res_id':clone_list[0], + 'view_id': False, + 'context':context, + 'type': 'ir.actions.act_window', + 'nodestroy': True, + 'target': 'current', + } + return abs + + def action_invoice_state(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state':'invoiced'}, context=context) + + def action_invoice(self, cr, uid, ids, context=None): + wf_service = netsvc.LocalService("workflow") + inv_ref = self.pool.get('account.invoice') + inv_line_ref = self.pool.get('account.invoice.line') + product_obj = self.pool.get('product.product') + inv_ids = [] + + for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context): + if order.invoice_id: + inv_ids.append(order.invoice_id.id) + continue + + if not order.partner_id: + raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.')) + + acc = order.partner_id.property_account_receivable.id + inv = { + 'name': order.name, + 'origin': order.name, + 'account_id': acc, + 'journal_id': order.sale_journal.id or None, + 'type': 'out_invoice', + 'reference': order.name, + 'partner_id': order.partner_id.id, + 'comment': order.note or '', + 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency + } + inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value']) + if not inv.get('account_id', None): + inv['account_id'] = acc + inv_id = inv_ref.create(cr, uid, inv, context=context) + + self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context) + inv_ids.append(inv_id) + for line in order.lines: + inv_line = { + 'invoice_id': inv_id, + 'product_id': line.product_id.id, + 'quantity': line.qty, + } + inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1] + inv_line.update(inv_line_ref.product_id_change(cr, uid, [], + line.product_id.id, + line.product_id.uom_id.id, + line.qty, partner_id = order.partner_id.id, + fposition_id=order.partner_id.property_account_position.id)['value']) + if line.product_id.description_sale: + inv_line['note'] = line.product_id.description_sale + 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_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) + + if not inv_ids: return {} + + mod_obj = self.pool.get('ir.model.data') + res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') + res_id = res and res[1] or False + return { + 'name': _('Customer Invoice'), + 'view_type': 'form', + 'view_mode': 'form', + 'view_id': [res_id], + 'res_model': 'account.invoice', + 'context': "{'type':'out_invoice'}", + 'type': 'ir.actions.act_window', + 'nodestroy': True, + 'target': 'current', + 'res_id': inv_ids and inv_ids[0] or False, + } + + def create_account_move(self, cr, uid, ids, context=None): + return self._create_account_move_line(cr, uid, ids, None, None, context=context) + + def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None): + # Tricky, via the workflow, we only have one id in the ids variable + """Create a account move line of order grouped by products or not.""" + account_move_obj = self.pool.get('account.move') + account_move_line_obj = self.pool.get('account.move.line') + account_period_obj = self.pool.get('account.period') + account_tax_obj = self.pool.get('account.tax') + user_proxy = self.pool.get('res.users') + property_obj = self.pool.get('ir.property') + + period = account_period_obj.find(cr, uid, context=context)[0] + + #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) + + if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): + raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!')) + + current_company = user_proxy.browse(cr, uid, uid, context=context).company_id + + grouped_data = {} + have_to_group_by = session and session.config_id.group_by or False + + def compute_tax(amount, tax, line): + if amount > 0: + tax_code_id = tax['base_code_id'] + tax_amount = line.price_subtotal * tax['base_sign'] + else: + tax_code_id = tax['ref_base_code_id'] + tax_amount = line.price_subtotal * tax['ref_base_sign'] + + return (tax_code_id, tax_amount,) + + for order in self.browse(cr, uid, ids, context=context): + if order.account_move: + continue + if order.state != 'paid': + continue + + user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id + + group_tax = {} + account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id + + order_account = order.partner_id and \ + order.partner_id.property_account_receivable and \ + order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id + + if move_id is None: + # Create an entry for the sale + move_id = account_move_obj.create(cr, uid, { + 'ref' : order.name, + 'journal_id': order.sale_journal.id, + }, context=context) + + def insert_data(data_type, values): + # if have_to_group_by: + + sale_journal_id = order.sale_journal.id + + # 'quantity': line.qty, + # 'product_id': line.product_id.id, + values.update({ + 'date': order.date_order[:10], + 'ref': order.name, + 'journal_id' : sale_journal_id, + 'period_id' : period, + 'move_id' : move_id, + 'company_id': user_company and user_company.id or False, + }) + + if data_type == 'product': + key = ('product', values['product_id'],) + elif data_type == 'tax': + key = ('tax', values['tax_code_id'],) + elif data_type == 'counter_part': + key = ('counter_part', values['partner_id'], values['account_id']) + else: + return + + grouped_data.setdefault(key, []) + + # if not have_to_group_by or (not grouped_data[key]): + # grouped_data[key].append(values) + # else: + # pass + + if have_to_group_by: + if not grouped_data[key]: + grouped_data[key].append(values) + else: + current_value = grouped_data[key][0] + current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) + current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) + current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + else: + grouped_data[key].append(values) + + # Create an move for each order line + + for line in order.lines: + tax_amount = 0 + taxes = [t for t in line.product_id.taxes_id] + computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes'] + + for tax in computed_taxes: + tax_amount += round(tax['amount'], 2) + group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id']) + + group_tax.setdefault(group_key, 0) + group_tax[group_key] += round(tax['amount'], 2) + + amount = line.price_subtotal + + # Search for the income account + if line.product_id.property_account_income.id: + income_account = line.product_id.property_account_income.id + elif line.product_id.categ_id.property_account_income_categ.id: + income_account = line.product_id.categ_id.property_account_income_categ.id + else: + raise osv.except_osv(_('Error!'), _('Please define income '\ + 'account for this product: "%s" (id:%d).') \ + % (line.product_id.name, line.product_id.id, )) + + # Empty the tax list as long as there is no tax code: + tax_code_id = False + tax_amount = 0 + while computed_taxes: + tax = computed_taxes.pop(0) + tax_code_id, tax_amount = compute_tax(amount, tax, line) + + # If there is one we stop + if tax_code_id: + break + + # Create a move for the line + insert_data('product', { + 'name': line.product_id.name, + 'quantity': line.qty, + 'product_id': line.product_id.id, + 'account_id': income_account, + 'credit': ((amount>0) and amount) or 0.0, + 'debit': ((amount<0) and -amount) or 0.0, + 'tax_code_id': tax_code_id, + 'tax_amount': tax_amount, + 'partner_id': order.partner_id and order.partner_id.id or False + }) + + # For each remaining tax with a code, whe create a move line + for tax in computed_taxes: + tax_code_id, tax_amount = compute_tax(amount, tax, line) + if not tax_code_id: + continue + + insert_data('tax', { + 'name': _('Tax'), + 'product_id':line.product_id.id, + 'quantity': line.qty, + 'account_id': income_account, + 'credit': 0.0, + 'debit': 0.0, + 'tax_code_id': tax_code_id, + 'tax_amount': tax_amount, + }) + + # Create a move for each tax group + (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3) + + for key, tax_amount in group_tax.items(): + tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context) + insert_data('tax', { + 'name': _('Tax') + ' ' + tax.name, + 'quantity': line.qty, + 'product_id': line.product_id.id, + 'account_id': key[account_pos], + 'credit': ((tax_amount>0) and tax_amount) or 0.0, + 'debit': ((tax_amount<0) and -tax_amount) or 0.0, + 'tax_code_id': key[tax_code_pos], + 'tax_amount': tax_amount, + }) + + # counterpart + insert_data('counter_part', { + 'name': _("Trade Receivables"), #order.name, + 'account_id': order_account, + 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, + 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, + 'partner_id': order.partner_id and order.partner_id.id or False + }) + + order.write({'state':'done', 'account_move': move_id}) + + for group_key, group_data in grouped_data.iteritems(): + for value in group_data: + account_move_line_obj.create(cr, uid, value, context=context) + + return True + + def action_payment(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state': 'payment'}, context=context) + + def action_paid(self, cr, uid, ids, context=None): + self.create_picking(cr, uid, ids, context=context) + self.write(cr, uid, ids, {'state': 'paid'}, context=context) + return True + + def action_cancel(self, cr, uid, ids, context=None): + self.write(cr, uid, ids, {'state': 'cancel'}, context=context) + return True + + def action_done(self, cr, uid, ids, context=None): + self.create_account_move(cr, uid, ids, context=context) + return True + +class account_bank_statement(osv.osv): + _inherit = 'account.bank.statement' + _columns= { + 'user_id': fields.many2one('res.users', 'User', readonly=True), + } + _defaults = { + 'user_id': lambda self,cr,uid,c={}: uid + } +account_bank_statement() + +class account_bank_statement_line(osv.osv): + _inherit = 'account.bank.statement.line' + _columns= { + 'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'), + } +account_bank_statement_line() + +class pos_order_line(osv.osv): + _name = "pos.order.line" + _description = "Lines of Point of Sale" + _rec_name = "product_id" + + def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None): + res = dict([(i, {}) for i in ids]) + account_tax_obj = self.pool.get('account.tax') + cur_obj = self.pool.get('res.currency') + for line in self.browse(cr, uid, ids, context=context): + taxes = line.product_id.taxes_id + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) + + cur = line.order_id.pricelist_id.currency_id + res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total']) + res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included']) + return res + + def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None): + context = context or {} + if not product_id: + return {} + if not pricelist: + raise osv.except_osv(_('No Pricelist !'), + _('You have to select a pricelist in the sale form !\n' \ + 'Please set one before choosing a product.')) + + price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist], + product_id, qty or 1.0, partner_id)[pricelist] + + result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context) + result['value']['price_unit'] = price + return result + + def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None): + result = {} + if not product: + return result + account_tax_obj = self.pool.get('account.tax') + cur_obj = self.pool.get('res.currency') + + 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) + + result['price_subtotal'] = taxes['total'] + result['price_subtotal_incl'] = taxes['total_included'] + return {'value': result} + + _columns = { + 'company_id': fields.many2one('res.company', 'Company', required=True), + 'name': fields.char('Line No', size=32, required=True), + 'notice': fields.char('Discount Notice', size=128), + 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True), + 'price_unit': fields.float(string='Unit Price', digits=(16, 2)), + 'qty': fields.float('Quantity', digits=(16, 2)), + 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True), + 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True), + 'discount': fields.float('Discount (%)', digits=(16, 2)), + 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'), + 'create_date': fields.datetime('Creation Date', readonly=True), + } + + _defaults = { + 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'), + 'qty': lambda *a: 1, + 'discount': lambda *a: 0.0, + 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, + } + + def copy_data(self, cr, uid, id, default=None, context=None): + if not default: + default = {} + default.update({ + 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line') + }) + return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context) + +class pos_category(osv.osv): + _name = 'pos.category' + _description = "Point of Sale Category" + _order = "sequence, name" + def _check_recursion(self, cr, uid, ids, context=None): + level = 100 + while len(ids): + cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),)) + ids = filter(None, map(lambda x:x[0], cr.fetchall())) + if not level: + return False + level -= 1 + return True + + _constraints = [ + (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) + ] + + def name_get(self, cr, uid, ids, context=None): + if not len(ids): + return [] + reads = self.read(cr, uid, ids, ['name','parent_id'], context=context) + res = [] + for record in reads: + name = record['name'] + if record['parent_id']: + name = record['parent_id'][1]+' / '+name + res.append((record['id'], name)) + return res + + def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): + res = self.name_get(cr, uid, ids, context=context) + return dict(res) + + def _get_image(self, cr, uid, ids, name, args, context=None): + result = dict.fromkeys(ids, False) + for obj in self.browse(cr, uid, ids, context=context): + result[obj.id] = tools.image_get_resized_images(obj.image) + return result + + def _set_image(self, cr, uid, id, name, value, args, context=None): + return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) + + _columns = { + 'name': fields.char('Name', size=64, required=True, translate=True), + 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'), + 'parent_id': fields.many2one('pos.category','Parent Category', select=True), + 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), + 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), + + # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail + # for at least one category, then we display a default image on the other, so that the buttons have consistent styling. + # In this case, the default image is set by the js code. + + 'image': fields.binary("Image", + help="This field holds the image used for the category. "\ + "The image is base64 encoded, and PIL-supported. "\ + "It is limited to a 1024x1024 px image."), + 'image_medium': fields.function(_get_image, fnct_inv=_set_image, + string="Medium-sized image", type="binary", multi="_get_image", + store = { + 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), + }, + help="Medium-sized image of the category. It is automatically "\ + "resized as a 180x180 px image, with aspect ratio preserved. "\ + "Use this field in form views or some kanban views."), + 'image_small': fields.function(_get_image, fnct_inv=_set_image, + string="Smal-sized image", type="binary", multi="_get_image", + store = { + 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), + }, + help="Small-sized image of the category. It is automatically "\ + "resized as a 50x50 px image, with aspect ratio preserved. "\ + "Use this field anywhere a small image is required."), + } + +import io, StringIO + +class ean_wizard(osv.osv_memory): + _name = 'pos.ean_wizard' + _columns = { + 'ean13_pattern': fields.char('Ean13 Pattern', size=32, required=True, translate=True), + } + def sanitize_ean13(self, cr, uid, ids, context): + for r in self.browse(cr,uid,ids): + ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern) + m = context.get('active_model') + m_id = context.get('active_id') + self.pool.get(m).write(cr,uid,[m_id],{'ean13':ean13}) + return { 'type' : 'ir.actions.act_window_close' } + +class product_product(osv.osv): + _inherit = 'product.product' + + + #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None): + # result = {} + # for obj in self.browse(cr, uid, ids, context=context): + # if not obj.product_image: + # result[obj.id] = False + # continue + + # image_stream = io.BytesIO(obj.product_image.decode('base64')) + # img = Image.open(image_stream) + # img.thumbnail((120, 100), Image.ANTIALIAS) + # img_stream = StringIO.StringIO() + # img.save(img_stream, "JPEG") + # result[obj.id] = img_stream.getvalue().encode('base64') + # return result + + _columns = { + 'income_pdt': fields.boolean('Point of Sale Cash In', help="This is a product you can use to put cash into a statement for the point of sale backend."), + 'expense_pdt': fields.boolean('Point of Sale Cash Out', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."), + 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', + help="If you want to sell this product through the point of sale, select the category it belongs to."), + 'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"), + } + _defaults = { + 'to_weight' : False, + } + + def edit_ean(self, cr, uid, ids, context): + return { + 'name': "Edit Ean", + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'pos.ean_wizard', + 'target' : 'new', + 'view_id': False, + 'context':context, + } + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/locadis/static/src/css/pos.css b/addons/locadis/static/src/css/pos.css new file mode 100644 index 00000000000..d9ec3ba7cee --- /dev/null +++ b/addons/locadis/static/src/css/pos.css @@ -0,0 +1,4 @@ + +.product-list-empty{ + text-align:center; +} diff --git a/addons/locadis/static/src/js/hello.js b/addons/locadis/static/src/js/hello.js new file mode 100644 index 00000000000..7deacfa994c --- /dev/null +++ b/addons/locadis/static/src/js/hello.js @@ -0,0 +1,12 @@ + +openerp.locadis = function(instance){ + console.log('Hello World!:',instance); + instance.point_of_sale.PosWidget = instance.point_of_sale.PosWidget.extend({ + init:function(parent,options){ + console.log('Tadaaam!',parent,options); + this._super(parent,options); + }, + }); + + /* REMOVE PRODUCTS FROM FIRST LEVEL IN SELF CHECKOUT */ +}; diff --git a/addons/locadis/static/src/js/main.js b/addons/locadis/static/src/js/main.js new file mode 100644 index 00000000000..1d0f41d8360 --- /dev/null +++ b/addons/locadis/static/src/js/main.js @@ -0,0 +1,24 @@ +openerp.locadis = function(instance){ + + var module = instance.point_of_sale; + var QWeb = instance.web.qweb; + + module.ProductListWidget = module.ProductListWidget.extend({ + template_empty: 'ProductEmptyListWidget', + get_category: function(){ + return this.getParent().product_categories_widget.category; + }, + renderElement: function(){ + var ss = this.pos_widget.screen_selector; + console.log('ss',ss); + if(this.get_category().name === 'Root' && ss && ss.get_user_mode() === 'cashier'){ + this.replaceElement(_.str.trim(QWeb.render(this.template_empty,{widget:this}))); + }else{ + this._super(); + } + }, + }); +}; + + + diff --git a/addons/locadis/static/src/xml/pos.xml b/addons/locadis/static/src/xml/pos.xml new file mode 100644 index 00000000000..f69c3b788df --- /dev/null +++ b/addons/locadis/static/src/xml/pos.xml @@ -0,0 +1,15 @@ + + + + +
+
+

Please scan an item

+ +
+
+
+
+ + +
From 0d70cdaaf51993a13acbf6964d34e71e601b0525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 23 Aug 2012 14:18:35 +0200 Subject: [PATCH 03/82] [IMP] locadis: dont_vidange and extra fields added to products bzr revid: fva@openerp.com-20120823121835-o3119dcmu9s0048t --- addons/locadis/__init__.py | 2 +- addons/locadis/__openerp__.py | 10 +++------- addons/locadis/static/src/css/pos.css | 4 +++- addons/locadis/static/src/xml/pos.xml | 3 +-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/addons/locadis/__init__.py b/addons/locadis/__init__.py index e57b362135b..60ffcac8393 100644 --- a/addons/locadis/__init__.py +++ b/addons/locadis/__init__.py @@ -19,7 +19,7 @@ # ############################################################################## -#import point_of_sale +import locadis # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/locadis/__openerp__.py b/addons/locadis/__openerp__.py index 056b8da7fcf..4ec8527fc7a 100644 --- a/addons/locadis/__openerp__.py +++ b/addons/locadis/__openerp__.py @@ -30,14 +30,10 @@ FIXME: product description """, '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': [], 'depends': ['point_of_sale'], - 'init_xml': [], - 'update_xml': [ - ], - 'demo_xml': [ - ], - 'test': [ + 'data':[ + 'locadis_views.xml', ], 'installable': True, 'application': True, diff --git a/addons/locadis/static/src/css/pos.css b/addons/locadis/static/src/css/pos.css index d9ec3ba7cee..075b4dccc27 100644 --- a/addons/locadis/static/src/css/pos.css +++ b/addons/locadis/static/src/css/pos.css @@ -1,4 +1,6 @@ - +.point-of-sale .screen p{ + font-size: 24px; +} .product-list-empty{ text-align:center; } diff --git a/addons/locadis/static/src/xml/pos.xml b/addons/locadis/static/src/xml/pos.xml index f69c3b788df..5ce23396db8 100644 --- a/addons/locadis/static/src/xml/pos.xml +++ b/addons/locadis/static/src/xml/pos.xml @@ -4,12 +4,11 @@
-

Please scan an item

+

Please scan your products

- From 28d177e520800af7ef6333765a562246174c9eb4 Mon Sep 17 00:00:00 2001 From: Tejas Tank Date: Thu, 30 Aug 2012 19:53:48 +0530 Subject: [PATCH 04/82] [FIX] i18n address format bzr revid: tta@openerp.com-20120830142348-vm37rbyceaup8zfq --- openerp/addons/base/res/res_partner.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 93d12228ebe..f70b45d9472 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -507,6 +507,40 @@ class res_partner(osv.osv): args['company_name'] = '' return address_format % args + def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + res = super(res_partner,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu) + if view_type == 'form': + fieldstyle = { 'state_id': {'placeholder': "State", 'style': "width:24%;", 'options': '{"no_open": true}', 'class': "oe_no_button"}, + 'city': {'placeholder': "City", 'style': "width: 40%;", 'options': '', 'class': ""}, + 'zip': {'placeholder': "ZIP", 'style': 'width:34%;', 'options': '', 'class': ""} + } + country_id = self.pool.get('res.users').browse(cr, uid, uid,context).company_id.country_id + if country_id and country_id.address_format: + address_format = country_id.address_format + else: + address_format = self._defaults['address_format'] + address_format = address_format.replace("_code","_id").replace("_name","_id") + fields = re.findall('(state_id|city|zip)', address_format) + + nodelist = [] + doc = etree.XML(res['arch']) + for field in ['city','state_id','zip']: + nodelist.append(doc.xpath("//field[@name='%s']" % (field))[0]) + + tot = len(fields) + for idx, node in enumerate(nodelist): + if idx < tot: + field = fields[idx] + node.set('name', fields[idx]) + node.set('style', fieldstyle[field]['style']) + node.set('placeholder', fieldstyle[field]['placeholder']) + node.set('options', fieldstyle[field]['options']) + node.set('class', fieldstyle[field]['class']) + else: + node.getparent().remove(node) + + res['arch'] = etree.tostring(doc) + return res # res.partner.address is deprecated; it is still there for backward compability only and will be removed in next version From 1a2f7a5131826ec6cba245292e9364454db62e8c Mon Sep 17 00:00:00 2001 From: Tejas Tank Date: Fri, 31 Aug 2012 16:08:30 +0530 Subject: [PATCH 05/82] IMP: i18n address format. bzr revid: tta@openerp.com-20120831103830-3551ioq85r61kh94 --- openerp/addons/base/res/res_partner.py | 41 +++++++++++++++----------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index f70b45d9472..830804cfae7 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -29,6 +29,7 @@ from tools.translate import _ import logging import pooler import pytz +from lxml import etree def _tz_get(self,cr,uid, context=None): return [(x, x) for x in pytz.all_timezones] @@ -508,28 +509,34 @@ class res_partner(osv.osv): return address_format % args def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - res = super(res_partner,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu) - if view_type == 'form': - fieldstyle = { 'state_id': {'placeholder': "State", 'style': "width:24%;", 'options': '{"no_open": true}', 'class': "oe_no_button"}, - 'city': {'placeholder': "City", 'style': "width: 40%;", 'options': '', 'class': ""}, - 'zip': {'placeholder': "ZIP", 'style': 'width:34%;', 'options': '', 'class': ""} - } - country_id = self.pool.get('res.users').browse(cr, uid, uid,context).company_id.country_id - if country_id and country_id.address_format: - address_format = country_id.address_format - else: - address_format = self._defaults['address_format'] - address_format = address_format.replace("_code","_id").replace("_name","_id") - fields = re.findall('(state_id|city|zip)', address_format) - + res = super(res_partner,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu) + if view_id and context and view_type == 'form': nodelist = [] doc = etree.XML(res['arch']) for field in ['city','state_id','zip']: - nodelist.append(doc.xpath("//field[@name='%s']" % (field))[0]) + nodes = doc.xpath("//field[@name='%s']" % (field)) + for node in nodes: + nodelist.append(node) + if not nodelist: + return res + + fieldstyle = { + 'state_id': {'placeholder': "State", 'style': "width:24%;", 'options': '{"no_open": true}', 'class': "oe_no_button"}, + 'city': {'placeholder': "City", 'style': "width: 40%;", 'options': '', 'class': ""}, + 'zip': {'placeholder': "ZIP", 'style': 'width:34%;', 'options': '', 'class': ""} + } + user_obj = self.pool.get('res.users') + country_id = user_obj.browse(cr, uid, uid,context).company_id.country_id + if country_id and country_id.address_format: + address_format = country_id.address_format + else: + address_format = "%(company_name)s\n%(street)s\n%(street2)s\n%(city)s,%(state_code)s %(zip)s\n%(country_name)s" + address_format = address_format.replace("_code","_id").replace("_name","_id") + fields = re.findall('(state_id|city|zip)', address_format) - tot = len(fields) + cfields = len(fields) for idx, node in enumerate(nodelist): - if idx < tot: + if idx < cfields: field = fields[idx] node.set('name', fields[idx]) node.set('style', fieldstyle[field]['style']) From 1597d57466e525f0a3de08714e37dc74674a1edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 3 Sep 2012 15:02:44 +0200 Subject: [PATCH 06/82] [FIX] locadis: probably forgot to add these files bzr revid: fva@openerp.com-20120903130244-j2l901o2x5pztpu8 --- addons/locadis/locadis.py | 51 ++++++++++++++++++++++++++++++++ addons/locadis/locadis_views.xml | 16 ++++++++++ 2 files changed, 67 insertions(+) create mode 100644 addons/locadis/locadis.py create mode 100644 addons/locadis/locadis_views.xml diff --git a/addons/locadis/locadis.py b/addons/locadis/locadis.py new file mode 100644 index 00000000000..fc079ab4288 --- /dev/null +++ b/addons/locadis/locadis.py @@ -0,0 +1,51 @@ + +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import pdb +import openerp +import addons +import openerp.addons.product.product + +import time +from datetime import datetime +from dateutil.relativedelta import relativedelta +import logging + +import netsvc +from osv import fields, osv +import tools +from tools.translate import _ +from decimal import Decimal +import decimal_precision as dp + +_logger = logging.getLogger(__name__) + +class product_product(osv.osv): + _inherit = 'product.product' + _columns = { + 'dont_vidange': fields.float('Dont Vidange', help="Le prix de ce produit inclus ce montant de vidange"), + 'extra': fields.char('Information Supplémentaire', help="Permet de stocker des informations supplémentaires sur le produit",size=256), + } + _defaults = { + 'dont_vidange': 0.0, + } + diff --git a/addons/locadis/locadis_views.xml b/addons/locadis/locadis_views.xml new file mode 100644 index 00000000000..c1c99100a8d --- /dev/null +++ b/addons/locadis/locadis_views.xml @@ -0,0 +1,16 @@ + + + + + product.locadis + product.product + + + + + + + + + + From c6e0e905b9c3bc8e452fde41579a6178afa4a6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 3 Sep 2012 16:34:45 +0200 Subject: [PATCH 07/82] [WIP] locadis: wip, need to merge bzr revid: fva@openerp.com-20120903143445-xf5cl43c9sijqdtw --- addons/locadis/locadis.py | 1 - addons/locadis/locadis_views.xml | 1 - addons/locadis/static/src/js/main.js | 130 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/addons/locadis/locadis.py b/addons/locadis/locadis.py index fc079ab4288..4d04825cba3 100644 --- a/addons/locadis/locadis.py +++ b/addons/locadis/locadis.py @@ -43,7 +43,6 @@ class product_product(osv.osv): _inherit = 'product.product' _columns = { 'dont_vidange': fields.float('Dont Vidange', help="Le prix de ce produit inclus ce montant de vidange"), - 'extra': fields.char('Information Supplémentaire', help="Permet de stocker des informations supplémentaires sur le produit",size=256), } _defaults = { 'dont_vidange': 0.0, diff --git a/addons/locadis/locadis_views.xml b/addons/locadis/locadis_views.xml index c1c99100a8d..fc8473b1c3c 100644 --- a/addons/locadis/locadis_views.xml +++ b/addons/locadis/locadis_views.xml @@ -8,7 +8,6 @@ - diff --git a/addons/locadis/static/src/js/main.js b/addons/locadis/static/src/js/main.js index 1d0f41d8360..fcd9ee78bf6 100644 --- a/addons/locadis/static/src/js/main.js +++ b/addons/locadis/static/src/js/main.js @@ -3,6 +3,136 @@ openerp.locadis = function(instance){ var module = instance.point_of_sale; var QWeb = instance.web.qweb; + console.log('PosModel?', module.PosModel); + + module.PosModel = module.PosModel.extend({ + // all this copy paste just to load the product.dont_vidange field ... :( + load_server_data: function(){ + console.log('loading'); + var self = this; + + var loaded = fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) + .pipe(function(users){ + self.set('user',users[0]); + + return fetch('res.company', + [ + 'currency_id', + 'email', + 'website', + 'company_registry', + 'vat', + 'name', + 'phone', + 'partner_id', + ], + [['id','=',users[0].company_id[0]]]); + }).pipe(function(companies){ + self.set('company',companies[0]); + + return fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]); + }).pipe(function(company_partners){ + self.get('company').contact_address = company_partners[0].contact_address; + + return fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]); + }).pipe(function(currencies){ + self.set('currency',currencies[0]); + + return fetch('product.uom', null, null); + }).pipe(function(units){ + self.set('units',units); + var units_by_id = {}; + for(var i = 0, len = units.length; i < len; i++){ + units_by_id[units[i].id] = units[i]; + } + self.set('units_by_id',units_by_id); + + return fetch('product.packaging', null, null); + }).pipe(function(packagings){ + self.set('product.packaging',packagings); + + return fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); + }).pipe(function(users){ + self.set('user_list',users); + + return fetch('account.tax', ['amount', 'price_include', 'type']); + }).pipe(function(taxes){ + self.set('taxes', taxes); + + return fetch( + 'pos.session', + ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'], + [['state', '=', 'opened'], ['user_id', '=', self.session.uid]] + ); + }).pipe(function(sessions){ + self.set('pos_session', sessions[0]); + + return fetch( + 'pos.config', + ['name','journal_ids','shop_id','journal_id', + 'iface_self_checkout', 'iface_led', 'iface_cashdrawer', + 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard', + 'iface_print_via_proxy','iface_cashdrawer','state','sequence_id','session_ids'], + [['id','=', self.get('pos_session').config_id[0]]] + ); + }).pipe(function(configs){ + var pos_config = configs[0]; + self.set('pos_config', pos_config); + self.iface_electronic_scale = !!pos_config.iface_electronic_scale; + self.iface_print_via_proxy = !!pos_config.iface_print_via_proxy; + self.iface_vkeyboard = !!pos_config.iface_vkeyboard; + self.iface_self_checkout = !!pos_config.iface_self_checkout; + self.iface_cashdrawer = !!pos_config.iface_cashdrawer; + + return fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]); + }).pipe(function(shops){ + self.set('shop',shops[0]); + + return fetch('pos.category', ['id','name','parent_id','child_id','image']) + }).pipe(function(categories){ + self.db.add_categories(categories); + + return fetch( + 'product.product', + ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', + 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', + 'description_sale', 'description','dont_vidange'], + [['pos_categ_id','!=', false],['sale_ok','=',true]], + {pricelist: self.get('shop').pricelist_id[0]} // context for price + ); + }).pipe(function(products){ + self.db.add_products(products); + + return fetch( + 'account.bank.statement', + ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], + [['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]] + ); + }).pipe(function(bank_statements){ + self.set('bank_statements', bank_statements); + + return fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]); + }).pipe(function(journals){ + self.set('journals',journals); + + // associate the bank statements with their journals. + var bank_statements = self.get('bank_statements'); + for(var i = 0, ilen = bank_statements.length; i < ilen; i++){ + for(var j = 0, jlen = journals.length; j < jlen; j++){ + if(bank_statements[i].journal_id[0] === journals[j].id){ + bank_statements[i].journal = journals[j]; + bank_statements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; + } + } + } + self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))}); + self.log_loaded_data(); + }); + + + return loaded; + }, + }); module.ProductListWidget = module.ProductListWidget.extend({ template_empty: 'ProductEmptyListWidget', get_category: function(){ From e0d5b6ac8bf8607eb8511e3a956a09cab069da06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 3 Sep 2012 16:43:54 +0200 Subject: [PATCH 08/82] [IMP] point_of_sale: fetch is now a method bzr revid: fva@openerp.com-20120903144354-8563uwq7x0cvqvrp --- addons/point_of_sale/static/src/js/models.js | 37 ++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 93860697492..cb258187b2a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,9 +1,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sale var QWeb = instance.web.qweb; - var fetch = function(model, fields, domain, ctx){ - return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() - }; // The PosModel contains the Point Of Sale's representation of the backend. // Since the PoS must work in standalone ( Without connection to the server ) @@ -70,15 +67,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }); }, + // helper function to load data from the server + fetch: function(model, fields, domain, ctx){ + return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() + }; // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. load_server_data: function(){ var self = this; - var loaded = fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) + var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) .pipe(function(users){ self.set('user',users[0]); - return fetch('res.company', + return self.fetch('res.company', [ 'currency_id', 'email', @@ -93,15 +94,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }).pipe(function(companies){ self.set('company',companies[0]); - return fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]); + return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]); }).pipe(function(company_partners){ self.get('company').contact_address = company_partners[0].contact_address; - return fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]); + return self.fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]); }).pipe(function(currencies){ self.set('currency',currencies[0]); - return fetch('product.uom', null, null); + return self.fetch('product.uom', null, null); }).pipe(function(units){ self.set('units',units); var units_by_id = {}; @@ -110,19 +111,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } self.set('units_by_id',units_by_id); - return fetch('product.packaging', null, null); + return self.fetch('product.packaging', null, null); }).pipe(function(packagings){ self.set('product.packaging',packagings); - return fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); + return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); }).pipe(function(users){ self.set('user_list',users); - return fetch('account.tax', ['amount', 'price_include', 'type']); + return self.fetch('account.tax', ['amount', 'price_include', 'type']); }).pipe(function(taxes){ self.set('taxes', taxes); - return fetch( + return self.fetch( 'pos.session', ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'], [['state', '=', 'opened'], ['user_id', '=', self.session.uid]] @@ -130,7 +131,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }).pipe(function(sessions){ self.set('pos_session', sessions[0]); - return fetch( + return self.fetch( 'pos.config', ['name','journal_ids','shop_id','journal_id', 'iface_self_checkout', 'iface_led', 'iface_cashdrawer', @@ -147,15 +148,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal self.iface_self_checkout = !!pos_config.iface_self_checkout; self.iface_cashdrawer = !!pos_config.iface_cashdrawer; - return fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]); + return self.fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]); }).pipe(function(shops){ self.set('shop',shops[0]); - return fetch('pos.category', ['id','name','parent_id','child_id','image']) + return self.fetch('pos.category', ['id','name','parent_id','child_id','image']) }).pipe(function(categories){ self.db.add_categories(categories); - return fetch( + return self.fetch( 'product.product', ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'], @@ -165,7 +166,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }).pipe(function(products){ self.db.add_products(products); - return fetch( + return self.fetch( 'account.bank.statement', ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], [['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]] @@ -173,7 +174,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }).pipe(function(bank_statements){ self.set('bank_statements', bank_statements); - return fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]); + return self.fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]); }).pipe(function(journals){ self.set('journals',journals); From d66519fda2935caaadc17ad5b10266af0cbc7870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 3 Sep 2012 16:59:58 +0200 Subject: [PATCH 09/82] [FIX] point_of_sale: I should have tested this more bzr revid: fva@openerp.com-20120903145958-c7i93lp8q8w8ilop --- addons/point_of_sale/static/src/js/models.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index cb258187b2a..caf3868482a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -70,7 +70,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // helper function to load data from the server fetch: function(model, fields, domain, ctx){ return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() - }; + }, // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. load_server_data: function(){ var self = this; From ee07aa224ca5a338e34358cc1842e5a759b9f2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 4 Sep 2012 17:46:03 +0200 Subject: [PATCH 10/82] [IMP] point_of_sale: added console ean input in debug mode bzr revid: fva@openerp.com-20120904154603-khymfg996jdgn9xr --- addons/point_of_sale/static/src/js/devices.js | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index e36070414b6..1c1902cdf8f 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -236,7 +236,20 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal this.price_prefix_set = attributes.price_prefix_set || {'23':''}; this.cashier_prefix_set = attributes.cashier_prefix_set || {'041':''}; this.client_prefix_set = attributes.client_prefix_set || {'042':''}; + + if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ + var self = this; + window.simulate_ean = function(ean,strict){ + ean = ean.toString(); + if(!strict){ + ean = self.sanitize_ean(ean); + } + console.log('SIMULATE EAN: ',ean); + self.on_ean(ean); + } + } }, + save_callbacks: function(){ var callbacks = {}; for(name in this.action_callback){ @@ -244,6 +257,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal } this.action_callback_stack.push(callbacks); }, + restore_callbacks: function(){ if(this.action_callback_stack.length){ var callbacks = this.action_callback_stack.pop(); @@ -380,6 +394,23 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal return parse_result; }, + on_ean: function(ean){ + var parse_result = this.parse_ean(ean); + + if (parse_result.type === 'error') { //most likely a checksum error, raise warning + console.warn('WARNING: barcode checksum error:',parse_result); + }else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product + if(this.action_callback['product']){ + this.action_callback['product'](parse_result); + } + //this.trigger("codebar",parse_result ); + }else{ + if(this.action_callback[parse_result.type]){ + this.action_callback[parse_result.type](parse_result); + } + } + }, + // starts catching keyboard events and tries to interpret codebar // calling the callbacks when needed. connect: function(){ @@ -410,21 +441,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal lastTimeStamp = new Date().getTime(); if (codeNumbers.length == 13) { //We have found what seems to be a valid codebar - var parse_result = self.parse_ean(codeNumbers.join('')); - - if (parse_result.type === 'error') { //most likely a checksum error, raise warning - console.warn('WARNING: barcode checksum error:',parse_result); - }else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product - if(self.action_callback['product']){ - self.action_callback['product'](parse_result); - } - //this.trigger("codebar",parse_result ); - }else{ - if(self.action_callback[parse_result.type]){ - self.action_callback[parse_result.type](parse_result); - } - } - + self.on_ean(codeNumbers.join('')); codeNumbers = []; } } else { From f24667b1f9e0175be293c0a219f1b366dd95d9ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 11:20:53 +0200 Subject: [PATCH 11/82] [FIX] point_of_sale: ean argument was wrongly passed to the proxy bzr revid: fva@openerp.com-20120905092053-9hj9sbscqrjbg0h0 --- addons/point_of_sale/controllers/main.py | 8 ++++---- addons/point_of_sale/static/src/js/devices.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 425a7e3d7f8..5e4d8f8a75a 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -72,19 +72,19 @@ class PointOfSaleController(openerpweb.Controller): return getattr(self, method)(request, **kwargs) @openerpweb.jsonrequest - def scan_item_success(self, request): + def scan_item_success(self, request, ean): """ A product has been scanned with success """ - print 'scan_item_success' + print 'scan_item_success: ' + str(ean) return @openerpweb.jsonrequest - def scan_item_error_unrecognized(self, request): + def scan_item_error_unrecognized(self, request, ean): """ A product has been scanned without success """ - print 'scan_item_error_unrecognized' + print 'scan_item_error_unrecognized: ' + str(ean) return @openerpweb.jsonrequest diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index 1c1902cdf8f..34efb993db3 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -57,13 +57,13 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal //a product has been scanned and recognized with success // ean is a parsed ean object scan_item_success: function(ean){ - this.message('scan_item_success',ean); + this.message('scan_item_success',{ean: ean}); }, // a product has been scanned but not recognized // ean is a parsed ean object scan_item_error_unrecognized: function(ean){ - this.message('scan_item_error_unrecognized',ean); + this.message('scan_item_error_unrecognized',{ean: ean}); }, //the client is asking for help From 49b1605b9846cc56d51998e5dc6c4178ab464c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 13:50:00 +0200 Subject: [PATCH 12/82] [FIX] point_of_sale: css fixes related to ul/li semantic css changes bzr revid: fva@openerp.com-20120905115000-dx2exlr3cnn2iszy --- addons/point_of_sale/static/src/css/pos.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index bfe2ef7f9dc..c43d5641ed6 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -367,7 +367,7 @@ /* ********* The product list ********* */ .point-of-sale .product-list { - padding:10px; + padding:10px !important; } .point-of-sale .product-list-scroller{ @@ -464,7 +464,7 @@ } .point-of-sale .category-list{ - padding:10px; + padding:10px !important; } /* d) the category button */ @@ -473,7 +473,7 @@ vertical-align: top; display: inline-block; font-size: 11px; - margin: 5px; + margin: 5px !important; width: 120px; height:120px; background:#fff; @@ -560,7 +560,7 @@ display: inline-block; line-height: 100px; font-size: 11px; - margin: 5px; + margin: 5px !important; width: 120px; height:120px; background:#fff; From 4b0cc507d78da894b41b18cdd2ff0637ea4f04be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 14:20:22 +0200 Subject: [PATCH 13/82] [FIX] fixes locadis views bzr revid: fva@openerp.com-20120905122022-x9ist4f080vp9sx3 --- addons/locadis/locadis_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/locadis/locadis_views.xml b/addons/locadis/locadis_views.xml index fc8473b1c3c..3b9b9d87195 100644 --- a/addons/locadis/locadis_views.xml +++ b/addons/locadis/locadis_views.xml @@ -4,7 +4,7 @@ product.locadis product.product - + From 2601dcd9f821ad4897bb8be69cd34df3d0d75215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 14:55:17 +0200 Subject: [PATCH 14/82] [FIX] show vidange info in order widget bzr revid: fva@openerp.com-20120905125517-jbrugjta39z9qxsm --- addons/locadis/static/src/js/main.js | 1 - addons/locadis/static/src/xml/pos.xml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/locadis/static/src/js/main.js b/addons/locadis/static/src/js/main.js index 59e53e7ae1a..e1c78f1b341 100644 --- a/addons/locadis/static/src/js/main.js +++ b/addons/locadis/static/src/js/main.js @@ -126,7 +126,6 @@ openerp.locadis = function(instance){ } } self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))}); - self.log_loaded_data(); }); diff --git a/addons/locadis/static/src/xml/pos.xml b/addons/locadis/static/src/xml/pos.xml index 0a5ce169048..ad983b96721 100644 --- a/addons/locadis/static/src/xml/pos.xml +++ b/addons/locadis/static/src/xml/pos.xml @@ -14,9 +14,9 @@ - +
  • - Vidange: + Vidange:
  • From bff5b81db3b5da0077e5d60902ea26fea81878b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 18:42:21 +0200 Subject: [PATCH 15/82] [FIX] fixing some regressions bzr revid: fva@openerp.com-20120905164221-hcigfd0kxdckgz94 --- addons/point_of_sale/controllers/main.py | 5 ++ addons/point_of_sale/static/src/css/pos.css | 4 +- addons/point_of_sale/static/src/js/db.js | 3 + addons/point_of_sale/static/src/js/devices.js | 5 ++ addons/point_of_sale/static/src/js/models.js | 16 +++++ addons/point_of_sale/static/src/js/screens.js | 64 ++++++++++++++++--- addons/point_of_sale/static/src/js/widgets.js | 13 ++-- addons/point_of_sale/static/src/xml/pos.xml | 8 +-- 8 files changed, 99 insertions(+), 19 deletions(-) diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 5e4d8f8a75a..e6976dd319c 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -166,4 +166,9 @@ class PointOfSaleController(openerpweb.Controller): print 'print_receipt' + str(receipt) return + @openerpweb.jsonrequest + def print_pdf_invoice(self, request, pdfinvoice): + print 'print_pdf_invoice' + str(pdfinvoice) + return + diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index c43d5641ed6..3f8b91a7df0 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -803,6 +803,8 @@ } .point-of-sale .scale-screen .product-picture img{ + max-width: 178px; + max-height:178px; vertical-align: middle; cursor:pointer; } @@ -854,7 +856,7 @@ .point-of-sale .goodbye-message{ position: absolute; left:50%; - top:30%; + top:40%; width:500px; height:400px; margin-left: -250px; diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 77f7307b48f..58ffa3373d6 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -223,9 +223,12 @@ 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/devices.js b/addons/point_of_sale/static/src/js/devices.js index 34efb993db3..46d11938128 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -212,6 +212,11 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal print_receipt: function(receipt){ this.message('print_receipt',{receipt: receipt}); }, + + // asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server ) + print_pdf_invoice: function(pdfinvoice){ + this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice}); + }, }); // this module interfaces with the barcode reader. It assumes the barcode reader diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index caf3868482a..5928caf7c77 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -227,6 +227,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order push_order: function(record) { + console.log('PUSHING NEW ORDER:',record); this.db.add_order(record); this.flush(); }, @@ -242,10 +243,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal // and remove the successfully sent ones from the db once // it has been confirmed that they have been sent correctly. flush: function() { + //TODO make the mutex work + console.log('FLUSH'); //this makes sure only one _int_flush is called at the same time + /* return this.flush_mutex.exec(_.bind(function() { return this._flush(0); }, this)); + */ + this._flush(0); }, // attempts to send an order of index 'index' in the list of order to send. The index // is used to skip orders that failed. do not call this method outside the mutex provided @@ -254,6 +260,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal var self = this; var orders = this.db.get_orders(); self.set('nbr_pending_operations',orders.length); + console.log('TRYING TO FLUSH ORDER:',index,'Of',orders.length); var order = orders[index]; if(!order){ @@ -269,6 +276,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }) .done(function(){ //remove from db if success + console.log('Order successfully sent'); self.db.remove_order(order.id); self._flush(index); }); @@ -547,6 +555,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.pos = attributes.pos; this.selected_orderline = undefined; this.screen_data = {}; // see ScreenSelector + this.receipt_type = 'receipt'; // 'receipt' || 'invoice' return this; }, generateUniqueId: function() { @@ -618,6 +627,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal getDueLeft: function() { return this.getTotal() - this.getPaidTotal(); }, + // sets the type of receipt 'receipt'(default) or 'invoice' + set_receipt_type: function(type){ + this.receipt_type = type; + }, + get_receipt_type: function(){ + return this.receipt_type; + }, // the client related to the current order. set_client: function(client){ this.set('client',client); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 9a2071c6335..6dcce2f75f9 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -357,6 +357,35 @@ 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; + var currentOrder = self.pos.get('selectedOrder'); + + this.$('.button.receipt').off('click').click(function(){ + currentOrder.set_receipt_type('receipt'); + self.pos_widget.screen_selector.set_current_screen('products'); + }); + + this.$('.button.invoice').off('click').click(function(){ + currentOrder.set_receipt_type('invoice'); + self.pos_widget.screen_selector.set_current_screen('products'); + }); + }, + get_client_name: function(){ + var client = this.pos.get('selectedOrder').get_client(); + if( client ){ + return client.name; + }else{ + return ''; + } + }, + }); + module.ErrorPopupWidget = module.PopUpWidget.extend({ template:'ErrorPopupWidget', show: function(){ @@ -492,9 +521,12 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var product = this.get_product(); return (product ? product.get('list_price') : 0) || 0; }, - get_product_image: function(){ + get_product_image_url: function() { var product = this.get_product(); - return product ? product.get('image') : undefined; + if(!product){ + return ""; + } + return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+product.get('id'); }, get_product_weight: function(){ return this.weight || 0; @@ -536,12 +568,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var cashregister = selfCheckoutRegisters[0] || this.pos.get('cashRegisters').models[0]; currentOrder.addPaymentLine(cashregister); - - self.pos.push_order(currentOrder.exportAsJSON()).then(function() { - currentOrder.destroy(); - self.pos.proxy.transaction_end(); - self.pos_widget.screen_selector.set_current_screen(self.next_screen); - }); + self.pos.push_order(currentOrder.exportAsJSON()) + currentOrder.destroy(); + self.pos.proxy.transaction_end(); + self.pos_widget.screen_selector.set_current_screen(self.next_screen); }else if(payment === 'payment_rejected'){ clearInterval(this.intervalID); //TODO show a tryagain thingie ? @@ -571,19 +601,33 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa show_numpad: false, show_leftpane: false, + barcode_product_action: function(ean){ + self.pos.proxy.transaction_start(); + this._super(ean); + }, barcode_client_action: function(ean){ + self.pos.proxy.transaction_start(); this._super(ean); - this.pos_widget.screen_selector.set_current_screen(this.next_screen); + this.pos_widget.screen_selector.show_popup('choose-receipt'); }, show: function(){ this._super(); var self = this; + + this.add_action_button({ + label: 'help', + icon: '/point_of_sale/static/src/img/icons/png48/help.png', + click: function(){ + $('.goodbye-message').css({opacity:1}).hide(); + }, + }); + $('.goodbye-message').css({opacity:1}).show(); setTimeout(function(){ $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();}); - },3000); + },5000); }, }); diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 6de31c88dfd..79e9d3398bc 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -667,7 +667,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, show: function(){ this.$el.show(); }, hide: function(){ this.$el.hide(); }, - }); @@ -758,6 +757,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.screen_selector.set_default_screen(); + window.screen_selector = self.screen_selector; + self.pos.barcode_reader.connect(); instance.webclient.set_content_full_screen(true); @@ -831,6 +832,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.error_session_popup = new module.ErrorNoSessionPopupWidget(this, {}); this.error_session_popup.appendTo($('.point-of-sale')); + this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {}); + this.choose_receipt_popup.appendTo($('.point-of-sale')); + // -------- Misc --------- this.notification = new module.SynchNotificationWidget(this,{}); @@ -890,6 +894,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'error': this.error_popup, 'error-product': this.error_product_popup, 'error-session': this.error_session_popup, + 'choose-receipt': this.choose_receipt_popup, }, default_client_screen: 'welcome', default_cashier_screen: 'products', @@ -965,9 +970,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, try_close: function() { var self = this; - self.pos.flush().then(function() { - self.close(); - }); + //TODO : do the close after the flush... + self.pos.flush() + self.close(); }, close: function() { var self = this; diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index ab5f99a149e..a7276e680b4 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -185,8 +185,8 @@ - - + + @@ -313,10 +313,10 @@ - + From b2cb4b841dae962f6752770fb07bb4bd65a5249d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 23:07:51 +0200 Subject: [PATCH 18/82] [FIX] point_of_sale: unrecognized_items error handling bzr revid: fva@openerp.com-20120905210751-7lbzkypzefy6zb3f --- addons/point_of_sale/static/src/js/screens.js | 4 ++-- addons/point_of_sale/static/src/xml/pos.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index df5b611b42d..e10633f999b 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -188,7 +188,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa return true; } } - this.pos.proxy.scan_item_unrecognized(ean); + this.pos.proxy.scan_item_error_unrecognized(ean); return false; }, @@ -206,7 +206,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa return true; } } - this.pos.proxy.scan_item_unrecognized(ean); + this.pos.proxy.scan_item_error_unrecognized(ean); return false; //TODO start the transaction }, diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 424f817c442..a3a75ab05e5 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -346,7 +346,7 @@ From 87f5de0b0772d088c671ffc8523c712b7acd3d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 5 Sep 2012 23:09:27 +0200 Subject: [PATCH 19/82] [WIP] locadis: temporary disabling of the cust barcode schemas bzr revid: fva@openerp.com-20120905210927-8su2f4yd0eyvyzuu --- addons/locadis/static/src/js/hello.js | 12 ------------ addons/locadis/static/src/js/main.js | 5 ++--- 2 files changed, 2 insertions(+), 15 deletions(-) delete mode 100644 addons/locadis/static/src/js/hello.js diff --git a/addons/locadis/static/src/js/hello.js b/addons/locadis/static/src/js/hello.js deleted file mode 100644 index 7deacfa994c..00000000000 --- a/addons/locadis/static/src/js/hello.js +++ /dev/null @@ -1,12 +0,0 @@ - -openerp.locadis = function(instance){ - console.log('Hello World!:',instance); - instance.point_of_sale.PosWidget = instance.point_of_sale.PosWidget.extend({ - init:function(parent,options){ - console.log('Tadaaam!',parent,options); - this._super(parent,options); - }, - }); - - /* REMOVE PRODUCTS FROM FIRST LEVEL IN SELF CHECKOUT */ -}; diff --git a/addons/locadis/static/src/js/main.js b/addons/locadis/static/src/js/main.js index e1c78f1b341..db7bd1f5ded 100644 --- a/addons/locadis/static/src/js/main.js +++ b/addons/locadis/static/src/js/main.js @@ -3,12 +3,9 @@ openerp.locadis = function(instance){ var module = instance.point_of_sale; var QWeb = instance.web.qweb; - console.log('PosModel?', module.PosModel); - module.PosModel = module.PosModel.extend({ // all this copy paste just to load the product.dont_vidange field ... :( load_server_data: function(){ - console.log('loading'); var self = this; var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) @@ -155,6 +152,7 @@ openerp.locadis = function(instance){ }, }); + /* module.BarcodeReader = module.BarcodeReader.extend({ parse_ean: function(ean){ var parse_result = { @@ -213,4 +211,5 @@ openerp.locadis = function(instance){ return parse_result; }, }); + */ }; From 6acf8ed15d59581228467886c1e026c557346523 Mon Sep 17 00:00:00 2001 From: "Twinkle Christian (OpenERP)" Date: Fri, 7 Sep 2012 17:38:04 +0530 Subject: [PATCH 20/82] [IMP]Remove serverwarning bzr revid: tch@tinyerp.com-20120907060555-554i3iemfwfh6lwt bzr revid: tch@tinyerp.com-20120907120804-e4z6k6iuzukaipz2 --- openerp/addons/base/base_data.xml | 637 +++++++++++++++++- openerp/addons/base/i18n/es_MX.po | 2 +- openerp/addons/base/res/res_users.py | 3 + .../addons/test_impex/tests/test_export.py | 2 +- .../addons/test_impex/tests/test_import.py | 4 +- 5 files changed, 643 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml index 37c360b5cd8..6f4af968ec0 100644 --- a/openerp/addons/base/base_data.xml +++ b/openerp/addons/base/base_data.xml @@ -1110,9 +1110,644 @@
    + + - + + USD + $ + 0.01 + 4 + + + + 1.2834 + + + + + + + VEF + Bs.F + 0.0001 + 4 + + + + 5.864 + + + + + + CAD + $ + 0.01 + 4 + + + + 1.3388 + + + + + + + CHF + CHF + 0.01 + 4 + + + + 1.3086 + + + + + + BRL + R$ + 0.01 + 4 + + + + 2.2344 + + + + + + CNY + ¥ + 0.01 + 4 + + + + 8.7556 + + + + + + + COP + $ + 0.01 + 4 + + + + 2933.8378 + + + + + + CZK + Kč + 0.01 + 4 + + + + 26.5634 + + + + + + DKK + kr + 0.01 + 4 + + + + 7.4445 + + + + + + + HUF + Ft + 0.01 + 4 + + + + 271.5621 + + + + + + IDR + Rp + 0.01 + 4 + + + + 14352.00 + + + + + 11796.39 + + + + + + LVL + Ls + 0.01 + 4 + + + + 0.7086 + + + + + + + NOK + kr + 0.01 + 4 + + + + 7.8668 + + + + + + XPF + XPF + 1.00 + 4 + + + + 119.331742 + + + + + + PAB + B/. + 0.01 + 4 + + + + 1.2676 + + + + + + PLN + zł + 0.01 + 4 + + + + 4.1005 + + + + + + SEK + kr + 0.01 + 4 + + + + 10.3004 + + + + + + GBP + £ + 0.01 + 4 + + + + 0.8333 + + + + + + ARS + $ + 0.01 + 4 + + + + 5.0881 + + + + + + INR + ₹ + 0.01 + 4 + + + + 59.9739 + + + + + + AUD + $ + 0.01 + 4 + + + + 1.4070 + + + + + + UAH + ₴ + 0.01 + 4 + + + + 10.1969 + + + + + + VND + ₫ + 0.01 + 4 + + + + 26330.01 + + + + + + HKD + $ + 0.01 + 4 + + + + 11.1608 + + + + + + JPY + ¥ + 0.01 + 4 + + + + 133.62 + + + + + + BGN + лв + 0.01 + 4 + + + + 1.9558 + + + + + + LTL + Lt + 0.01 + 4 + + + + 3.4528 + + + + + + RON + lei + 0.01 + 4 + + + + 4.2253 + + + + + + HRK + kn + 0.01 + 4 + + + + 7.2936 + + + + + + RUB + руб + 0.01 + 4 + + + + 43.16 + + + + + + TRY + TL + 0.01 + 4 + + + + 2.1411 + + + + + + KRW + ₩ + 0.01 + 4 + + + + 1662.37 + + + + + + MXN + $ + 0.01 + 4 + + + + 18.6664 + + + + + + MYR + RM + 0.01 + 4 + + + + 4.8887 + + + + + + NZD + $ + 0.01 + 4 + + + + 1.9764 + + + + + + PHP + Php + 0.01 + 4 + + + + 66.1 + + + + + + SGD + $ + 0.01 + 4 + + + + 2.0126 + + + + + + THB + ฿ + 0.01 + 4 + + + + 47.779 + + + + + + ZAR + R + 0.01 + 4 + + + + 10.5618 + + + + + + International Bank + + + + CRC + 0.01 + 4 + ¢ + + + + 691.3153 + + + + + + localhost + localhost + + + + + + MUR + Rs + 0.01 + 4 + + + + 40.28 + + + + + + XOF + CFA + 1 + 4 + + + + 655.957 + + + + + + XAF + FCFA + 1 + 4 + + + + 655.957 + + + + + + UGX + USh + 1 + 4 + + + + 3401.91388 + + + + + + HNL + L + 0.01 + 4 + + + + 25 + + + + + + + CLP + $ + 0.01 + 4 + + + + 710 + + + + + + + UYU + $ + 0.01 + 4 + + + + + + 28.36 + + diff --git a/openerp/addons/base/i18n/es_MX.po b/openerp/addons/base/i18n/es_MX.po index cc9d519e705..dbc674d754a 100644 --- a/openerp/addons/base/i18n/es_MX.po +++ b/openerp/addons/base/i18n/es_MX.po @@ -14,7 +14,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2012-09-08 04:54+0000\n" +"X-Launchpad-Export-Date: 2012-09-07 04:55+0000\n" "X-Generator: Launchpad (build 15914)\n" #. module: base diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index d0f07daa004..8a80a171612 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -157,6 +157,9 @@ class res_users(osv.osv): 'user_email': fields.related('email', type='char', deprecated='Use the email field instead of user_email. This field will be removed with OpenERP 7.1.'), } + _sql_constraints = [ + ('email_uniq', 'UNIQUE (user_email)', 'You can not have two users with the same email!') + ] def on_change_company_id(self, cr, uid, ids, company_id): return {'warning' : { diff --git a/openerp/tests/addons/test_impex/tests/test_export.py b/openerp/tests/addons/test_impex/tests/test_export.py index 7e67ba581a1..26219b36d3b 100644 --- a/openerp/tests/addons/test_impex/tests/test_export.py +++ b/openerp/tests/addons/test_impex/tests/test_export.py @@ -245,7 +245,7 @@ class test_selection(CreatorCase): 'translatable': True, 'date_format': '%d.%m.%Y', 'decimal_point': ',', - 'thousand_sep': ' ', + 'thousands_sep': ' ', }) Translations = self.registry('ir.translation') for source, value in self.translations_fr: diff --git a/openerp/tests/addons/test_impex/tests/test_import.py b/openerp/tests/addons/test_impex/tests/test_import.py index 4712d53feff..a186323c754 100644 --- a/openerp/tests/addons/test_impex/tests/test_import.py +++ b/openerp/tests/addons/test_impex/tests/test_import.py @@ -393,7 +393,7 @@ class test_selection(ImporterCase): 'translatable': True, 'date_format': '%d.%m.%Y', 'decimal_point': ',', - 'thousand_sep': ' ', + 'thousands_sep': ' ', }) Translations = self.registry('ir.translation') for source, value in self.translations_fr: @@ -466,7 +466,7 @@ class test_selection_function(ImporterCase): 'translatable': True, 'date_format': '%d.%m.%Y', 'decimal_point': ',', - 'thousand_sep': ' ', + 'thousands_sep': ' ', }) Translations = self.registry('ir.translation') for source, value in self.translations_fr: From 83fc106eaad1b364aa301d872691e9340b9f60db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 10 Sep 2012 17:48:28 +0200 Subject: [PATCH 21/82] [FIX] point_of_sale: coherent image url handling bzr revid: fva@openerp.com-20120910154828-4k699hi3lu8lgxvu --- addons/point_of_sale/static/src/js/models.js | 6 ++++-- addons/point_of_sale/static/src/js/screens.js | 7 ------- addons/point_of_sale/static/src/js/widgets.js | 5 +---- addons/point_of_sale/static/src/xml/pos.xml | 7 ++++--- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 5928caf7c77..b5792f0124d 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -48,7 +48,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal 'units': null, 'units_by_id': null, - 'selectedOrder': undefined, + 'selectedOrder': null, }); this.get('orders').bind('remove', function(){ self.on_removed_order(); }); @@ -310,6 +310,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }); module.Product = Backbone.Model.extend({ + get_image_url: function(){ + return '/web/binary/image?session_id='instance.session.session_id+'&model=product.product&field=image&id='+this.get('id'); + }, }); module.ProductCollection = Backbone.Collection.extend({ @@ -339,7 +342,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal get_discount: function(){ return this.discount; }, - // FIXME get_product_type: function(){ return this.type; }, diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index e10633f999b..2ead7333bed 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -521,13 +521,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var product = this.get_product(); return (product ? product.get('list_price') : 0) || 0; }, - get_product_image_url: function() { - var product = this.get_product(); - if(!product){ - return ""; - } - return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+product.get('id'); - }, get_product_weight: function(){ return this.weight || 0; }, diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 79e9d3398bc..2aaf9098329 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -284,12 +284,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.click_product_action = options.click_product_action; }, // returns the url of the product thumbnail - get_image_url: function() { - return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+this.model.get('id'); - }, renderElement: function() { this._super(); - this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.get_image_url())); + this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.model.get_image_url())); var self = this; $("a", this.$el).click(function(e){ if(self.click_product_action){ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index a3a75ab05e5..46dc4712937 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -185,8 +185,8 @@ - - + + @@ -346,7 +346,8 @@ From 6c542b03308b148f0ba7d84410ddbf8ec45ced06 Mon Sep 17 00:00:00 2001 From: Jigar Amin - OpenERP Date: Tue, 11 Sep 2012 10:12:31 +0530 Subject: [PATCH 22/82] [IMP] Added the Print workflow option for the debug view bzr revid: jam@tinyerp.com-20120911044231-15aoi4ow7dtc1gfx --- addons/web/static/src/js/views.js | 2 ++ addons/web/static/src/xml/base.xml | 1 + 2 files changed, 3 insertions(+) diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 90709276bdf..d54e0194f38 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -836,6 +836,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ } }); break; + case 'print_workflow': + break; default: if (val) { console.log("No debug handler for ", val); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index d067b7586fb..9ec53a89863 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -487,6 +487,7 @@ + From 4db2bc0c43706bf05525851f7d1c45a31abe92b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 11 Sep 2012 12:18:30 +0200 Subject: [PATCH 23/82] [FIX] point_of_sale: help wasn't always cancelled when popup closed bzr revid: fva@openerp.com-20120911101830-dvxi9sg5oan5r5nc --- addons/point_of_sale/static/src/js/models.js | 2 +- addons/point_of_sale/static/src/js/screens.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index b5792f0124d..49521377ccb 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -311,7 +311,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal module.Product = Backbone.Model.extend({ get_image_url: function(){ - return '/web/binary/image?session_id='instance.session.session_id+'&model=product.product&field=image&id='+this.get('id'); + return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+this.get('id'); }, }); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 2ead7333bed..3b580868e6b 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -65,6 +65,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }, close_popup: function(){ if(this.current_popup){ + this.current_popup.close(); this.current_popup.hide(); this.current_popup = null; } @@ -336,6 +337,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.$el.show(); } }, + /* called before hide, when a popup is closed */ + close: function(){ + }, + /* hides the popup. keep in mind that this is called in the initialization pass of the + * pos instantiation, so you don't want to do anything fancy in here */ hide: function(){ if(this.$el){ this.$el.hide(); @@ -352,9 +358,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.$el.find('.button').off('click').click(function(){ self.pos_widget.screen_selector.close_popup(); - self.pos.proxy.help_canceled(); }); }, + close:function(){ + this.pos.proxy.help_canceled(); + }, }); module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({ From 6bbc6a90689914db16592c4c80d8f450e9a27410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Tue, 11 Sep 2012 12:23:52 +0200 Subject: [PATCH 24/82] [FIX] point_of_sale: the locadis module should not have been merged here ... bzr revid: fva@openerp.com-20120911102352-lia0c930ao1t19nw --- addons/locadis/__init__.py | 25 - addons/locadis/__openerp__.py | 52 - addons/locadis/locadis.py | 50 - addons/locadis/locadis_views.xml | 15 - addons/locadis/point_of_sale.py | 1303 ------------------------- addons/locadis/static/src/css/pos.css | 6 - addons/locadis/static/src/js/main.js | 215 ---- addons/locadis/static/src/xml/pos.xml | 25 - 8 files changed, 1691 deletions(-) delete mode 100644 addons/locadis/__init__.py delete mode 100644 addons/locadis/__openerp__.py delete mode 100644 addons/locadis/locadis.py delete mode 100644 addons/locadis/locadis_views.xml delete mode 100644 addons/locadis/point_of_sale.py delete mode 100644 addons/locadis/static/src/css/pos.css delete mode 100644 addons/locadis/static/src/js/main.js delete mode 100644 addons/locadis/static/src/xml/pos.xml diff --git a/addons/locadis/__init__.py b/addons/locadis/__init__.py deleted file mode 100644 index 60ffcac8393..00000000000 --- a/addons/locadis/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import locadis - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/locadis/__openerp__.py b/addons/locadis/__openerp__.py deleted file mode 100644 index 4ec8527fc7a..00000000000 --- a/addons/locadis/__openerp__.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - - -{ - 'name': 'Locadis Point Of Sale', - 'version': '1.0.1', - 'category': 'Point Of Sale', - "sequence": 6, - "summary": "Point of sale extensions for Locadis", - 'description': """ -FIXME: product description - """, - 'author': 'OpenERP SA', - 'images': [], - 'depends': ['point_of_sale'], - 'data':[ - 'locadis_views.xml', - ], - 'installable': True, - 'application': True, - # Web client - 'js': [ - 'static/src/js/main.js' - ], - 'css': [ - 'static/src/css/pos.css' - ], - 'qweb': [ - 'static/src/xml/pos.xml' - ], - 'auto_install': False, -} -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/locadis/locadis.py b/addons/locadis/locadis.py deleted file mode 100644 index 4d04825cba3..00000000000 --- a/addons/locadis/locadis.py +++ /dev/null @@ -1,50 +0,0 @@ - -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import pdb -import openerp -import addons -import openerp.addons.product.product - -import time -from datetime import datetime -from dateutil.relativedelta import relativedelta -import logging - -import netsvc -from osv import fields, osv -import tools -from tools.translate import _ -from decimal import Decimal -import decimal_precision as dp - -_logger = logging.getLogger(__name__) - -class product_product(osv.osv): - _inherit = 'product.product' - _columns = { - 'dont_vidange': fields.float('Dont Vidange', help="Le prix de ce produit inclus ce montant de vidange"), - } - _defaults = { - 'dont_vidange': 0.0, - } - diff --git a/addons/locadis/locadis_views.xml b/addons/locadis/locadis_views.xml deleted file mode 100644 index 3b9b9d87195..00000000000 --- a/addons/locadis/locadis_views.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - product.locadis - product.product - - - - - - - - - diff --git a/addons/locadis/point_of_sale.py b/addons/locadis/point_of_sale.py deleted file mode 100644 index f4d32985688..00000000000 --- a/addons/locadis/point_of_sale.py +++ /dev/null @@ -1,1303 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import pdb -import openerp -import addons -import openerp.addons.product.product - -import time -from datetime import datetime -from dateutil.relativedelta import relativedelta -import logging - -import netsvc -from osv import fields, osv -import tools -from tools.translate import _ -from decimal import Decimal -import decimal_precision as dp - -_logger = logging.getLogger(__name__) - -class pos_config(osv.osv): - _name = 'pos.config' - - POS_CONFIG_STATE = [ - ('active', 'Active'), - ('inactive', 'Inactive'), - ('deprecated', 'Deprecated') - ] - - _columns = { - 'name' : fields.char('Point of Sale Name', size=32, select=1, - required=True, help="An internal identification of the point of sale"), - 'journal_ids' : fields.many2many('account.journal', 'pos_config_journal_rel', - 'pos_config_id', 'journal_id', 'Available Payment Methods', - domain="[('journal_user', '=', True )]",), - 'shop_id' : fields.many2one('sale.shop', 'Shop', - required=True), - 'journal_id' : fields.many2one('account.journal', 'Sale Journal', - domain=[('type', '=', 'sale')], - help="Accounting journal used to post sales entries."), - 'iface_self_checkout' : fields.boolean('Self Checkout Mode', - help="Check this if this point of sale should open by default in a self checkout mode. If unchecked, OpenERP uses the normal cashier mode by default."), - 'iface_cashdrawer' : fields.boolean('Cashdrawer Interface'), - 'iface_payment_terminal' : fields.boolean('Payment Terminal Interface'), - 'iface_electronic_scale' : fields.boolean('Electronic Scale Interface'), - 'iface_vkeyboard' : fields.boolean('Virtual KeyBoard Interface'), - 'iface_print_via_proxy' : fields.boolean('Print via Proxy'), - - 'state' : fields.selection(POS_CONFIG_STATE, 'State', required=True, readonly=True), - 'sequence_id' : fields.many2one('ir.sequence', 'Order IDs Sequence', readonly=True, - help="This sequence is automatically created by OpenERP but you can change it "\ - "to customize the reference numbers of your orders."), - 'session_ids': fields.one2many('pos.session', 'config_id', 'Sessions'), - 'group_by' : fields.boolean('Group Journal Items', help="Check this if you want to group the Journal Items by Product while closing a Session"), - } - - def name_get(self, cr, uid, ids, context=None): - result = [] - states = { - 'opening_control': _('Opening Control'), - 'opened': _('In Progress'), - 'closing_control': _('Closing Control'), - 'closed': _('Closed & Posted'), - } - for record in self.browse(cr, uid, ids, context=context): - if (not record.session_ids) or (record.session_ids[0].state=='closed'): - result.append((record.id, record.name+' ('+_('not used')+')')) - continue - session = record.session_ids[0] - result.append((record.id, record.name + ' ('+session.user_id.name+')')) #, '+states[session.state]+')')) - return result - - - def _default_payment_journal(self, cr, uid, context=None): - res = self.pool.get('account.journal').search(cr, uid, [('type', 'in', ('bank','cash'))], limit=2) - return res or [] - - def _default_sale_journal(self, cr, uid, context=None): - res = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale')], limit=1) - return res and res[0] or False - - def _default_shop(self, cr, uid, context=None): - res = self.pool.get('sale.shop').search(cr, uid, []) - return res and res[0] or False - - _defaults = { - 'state' : POS_CONFIG_STATE[0][0], - 'shop_id': _default_shop, - 'journal_id': _default_sale_journal, - 'journal_ids': _default_payment_journal, - 'group_by' : True, - } - - def set_active(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'active'}, context=context) - - def set_inactive(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'inactive'}, context=context) - - def set_deprecate(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'deprecated'}, context=context) - - def create(self, cr, uid, values, context=None): - proxy = self.pool.get('ir.sequence') - sequence_values = dict( - name='PoS %s' % values['name'], - padding=5, - prefix="%s/" % values['name'], - ) - sequence_id = proxy.create(cr, uid, sequence_values, context=context) - values['sequence_id'] = sequence_id - return super(pos_config, self).create(cr, uid, values, context=context) - - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - if obj.sequence_id: - obj.sequence_id.unlink() - return super(pos_config, self).unlink(cr, uid, ids, context=context) - -class pos_session(osv.osv): - _name = 'pos.session' - _order = 'id desc' - - POS_SESSION_STATE = [ - ('opening_control', 'Opening Control'), # Signal open - ('opened', 'In Progress'), # Signal closing - ('closing_control', 'Closing Control'), # Signal close - ('closed', 'Closed & Posted'), - ] - - def _compute_cash_journal_id(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, False) - for record in self.browse(cr, uid, ids, context=context): - for st in record.statement_ids: - if st.journal_id.type == 'cash': - result[record.id] = st.journal_id.id - break - return result - - def _compute_cash_register_id(self, cr, uid, ids, fieldnames, args, context=None): - result = dict.fromkeys(ids, False) - for record in self.browse(cr, uid, ids, context=context): - for st in record.statement_ids: - if st.journal_id.type == 'cash': - result[record.id] = st.id - break - return result - - def _compute_controls(self, cr, uid, ids, fieldnames, args, context=None): - result = {} - - for record in self.browse(cr, uid, ids, context=context): - has_opening_control = False - has_closing_control = False - - for journal in record.config_id.journal_ids: - if journal.opening_control == True: - has_opening_control = True - if journal.closing_control == True: - has_closing_control = True - - if has_opening_control and has_closing_control: - break - - values = { - 'has_opening_control': has_opening_control, - 'has_closing_control': has_closing_control, - } - result[record.id] = values - - return result - - _columns = { - 'config_id' : fields.many2one('pos.config', 'Point of Sale', - help="The physical point of sale you will use.", - required=True, - select=1, - domain="[('state', '=', 'active')]", - ), - - 'name' : fields.char('Session ID', size=32, required=True, readonly=True), - 'user_id' : fields.many2one('res.users', 'Responsible', - required=True, - select=1, - readonly=True, - states={'opening_control' : [('readonly', False)]} - ), - 'start_at' : fields.datetime('Opening Date', readonly=True), - 'stop_at' : fields.datetime('Closing Date', readonly=True), - - 'state' : fields.selection(POS_SESSION_STATE, 'State', - required=True, readonly=True, - select=1), - - 'cash_journal_id' : fields.function(_compute_cash_journal_id, method=True, - type='many2one', relation='account.journal', - string='Cash Journal', store=True), - 'cash_register_id' : fields.function(_compute_cash_register_id, method=True, - type='many2one', relation='account.bank.statement', - string='Cash Register', store=True), - - 'opening_details_ids' : fields.related('cash_register_id', 'opening_details_ids', - type='one2many', relation='account.cashbox.line', - string='Opening Cash Control'), - 'details_ids' : fields.related('cash_register_id', 'details_ids', - type='one2many', relation='account.cashbox.line', - string='Cash Control'), - - 'cash_register_balance_end_real' : fields.related('cash_register_id', 'balance_end_real', - type='float', - digits_compute=dp.get_precision('Account'), - string="Ending Balance", - help="Computed using the cash control lines", - readonly=True), - 'cash_register_balance_start' : fields.related('cash_register_id', 'balance_start', - type='float', - digits_compute=dp.get_precision('Account'), - string="Starting Balance", - help="Computed using the cash control at the opening.", - readonly=True), - 'cash_register_total_entry_encoding' : fields.related('cash_register_id', 'total_entry_encoding', - string='Total Cash Transaction', - readonly=True), - 'cash_register_balance_end' : fields.related('cash_register_id', 'balance_end', - type='float', - digits_compute=dp.get_precision('Account'), - string="Computed Balance", - help="Computed with the initial cash control and the sum of all payments.", - readonly=True), - 'cash_register_difference' : fields.related('cash_register_id', 'difference', - type='float', - string='Difference', - help="Difference between the counted cash control at the closing and the computed balance.", - readonly=True), - - 'journal_ids' : fields.related('config_id', 'journal_ids', - type='many2many', - readonly=True, - relation='account.journal', - string='Available Payment Methods'), - 'order_ids' : fields.one2many('pos.order', 'session_id', 'Orders'), - - 'statement_ids' : fields.one2many('account.bank.statement', 'pos_session_id', 'Bank Statement', readonly=True), - 'has_opening_control' : fields.function(_compute_controls, string='Has Opening Control', multi='control', type='boolean'), - 'has_closing_control' : fields.function(_compute_controls, string='Has Closing Control', multi='control', type='boolean'), - } - - _defaults = { - 'name' : '/', - 'user_id' : lambda obj, cr, uid, context: uid, - 'state' : 'opening_control', - } - - _sql_constraints = [ - ('uniq_name', 'unique(name)', "The name of this POS Session must be unique !"), - ] - - def _check_unicity(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=None): - # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user - domain = [ - ('state', 'not in', ('closed','closing_control')), - ('user_id', '=', uid) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - def _check_pos_config(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=None): - domain = [ - ('state', '!=', 'closed'), - ('config_id', '=', session.config_id.id) - ] - count = self.search_count(cr, uid, domain, context=context) - if count>1: - return False - return True - - _constraints = [ - (_check_unicity, "You cannot create two active sessions with the same responsible!", ['user_id', 'state']), - (_check_pos_config, "You cannot create two active sessions related to the same point of sale!", ['config_id']), - ] - - def create(self, cr, uid, values, context=None): - config_id = values.get('config_id', False) or False - if config_id: - # journal_id is not required on the pos_config because it does not - # exists at the installation. If nothing is configured at the - # installation we do the minimal configuration. Impossible to do in - # the .xml files as the CoA is not yet installed. - jobj = self.pool.get('pos.config') - pos_config = jobj.browse(cr, uid, config_id, context=context) - if not pos_config.journal_id: - jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id'] - if jid: - jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context) - else: - raise osv.except_osv( _('error!'), - _("Unable to open the session. You have to assign a sale journal to your point of sale.")) - - # define some cash journal if no payment method exists - if not pos_config.journal_ids: - cashids = self.pool.get('account.journal').search(cr, uid, [('journal_user','=',True)], context=context) - if not cashids: - cashids = self.pool.get('account.journal').search(cr, uid, [('type','=','cash')], context=context) - self.pool.get('account.journal').write(cr, uid, cashids, {'journal_user': True}) - jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]}) - - - pos_config = jobj.browse(cr, uid, config_id, context=context) - bank_statement_ids = [] - for journal in pos_config.journal_ids: - bank_values = { - 'journal_id' : journal.id, - 'user_id' : uid, - } - statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context) - bank_statement_ids.append(statement_id) - - values.update({ - 'name' : pos_config.sequence_id._next(), - 'statement_ids' : [(6, 0, bank_statement_ids)], - 'config_id': config_id - }) - - return super(pos_session, self).create(cr, uid, values, context=context) - - def unlink(self, cr, uid, ids, context=None): - for obj in self.browse(cr, uid, ids, context=context): - for statement in obj.statement_ids: - statement.unlink(context=context) - return True - - def wkf_action_open(self, cr, uid, ids, context=None): - # second browse because we need to refetch the data from the DB for cash_register_id - for record in self.browse(cr, uid, ids, context=context): - values = {} - if not record.start_at: - values['start_at'] = time.strftime('%Y-%m-%d %H:%M:%S') - values['state'] = 'opened' - record.write(values, context=context) - for st in record.statement_ids: - st.button_open(context=context) - - return self.open_frontend_cb(cr, uid, ids, context=context) - - def wkf_action_opening_control(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state' : 'opening_control'}, context=context) - - def wkf_action_closing_control(self, cr, uid, ids, context=None): - for session in self.browse(cr, uid, ids, context=context): - for statement in session.statement_ids: - if statement.id <> session.cash_register_id.id: - if statement.balance_end<>statement.balance_end_real: - self.pool.get('account.bank.statement').write(cr, uid, - [statement.id], {'balance_end_real': statement.balance_end}) - return self.write(cr, uid, ids, {'state' : 'closing_control', 'stop_at' : time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) - - def wkf_action_close(self, cr, uid, ids, context=None): - # Close CashBox - bsl = self.pool.get('account.bank.statement.line') - for record in self.browse(cr, uid, ids, context=context): - for st in record.statement_ids: - if abs(st.difference) > st.journal_id.amount_authorized_diff: - # The pos manager can close statements with maximums. - if not self.pool.get('ir.model.access').check_groups(cr, uid, "point_of_sale.group_pos_manager"): - raise osv.except_osv( _('Error!'), - _("Your ending balance is too different from the theorical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it.") % (st.difference, st.journal_id.amount_authorized_diff)) - if st.difference: - if st.difference > 0.0: - name= _('Point of Sale Profit') - account_id = st.journal_id.profit_account_id.id - else: - account_id = st.journal_id.loss_account_id.id - name= _('Point of Sale Loss') - if not account_id: - raise osv.except_osv( _('Error!'), - _("Please set your profit and loss accounts on your payment method '%s'. This will allow OpenERP to post the difference of %.2f in your ending balance. To close this session, you can update the 'Closing Cash Control' to avoid any difference.") % (st.journal_id.name,st.difference)) - bsl.create(cr, uid, { - 'statement_id': st.id, - 'amount': st.difference, - 'ref': record.name, - 'name': name, - 'account_id': account_id - }, context=context) - - getattr(st, 'button_confirm_%s' % st.journal_id.type)(context=context) - self._confirm_orders(cr, uid, ids, context=context) - self.write(cr, uid, ids, {'state' : 'closed'}, context=context) - - obj = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'point_of_sale', 'menu_point_root')[1] - return { - 'type' : 'ir.actions.client', - 'name' : 'Point of Sale Menu', - 'tag' : 'reload', - 'params' : {'menu_id': obj}, - } - - def _confirm_orders(self, cr, uid, ids, context=None): - wf_service = netsvc.LocalService("workflow") - - for session in self.browse(cr, uid, ids, context=context): - order_ids = [order.id for order in session.order_ids if order.state == 'paid'] - - move_id = self.pool.get('account.move').create(cr, uid, {'ref' : session.name, 'journal_id' : session.config_id.journal_id.id, }, context=context) - - self.pool.get('pos.order')._create_account_move_line(cr, uid, order_ids, session, move_id, context=context) - - for order in session.order_ids: - if order.state != 'paid': - raise osv.except_osv( - _('Error!'), - _("You cannot confirm all orders of this session, because they have not the 'paid' status")) - else: - wf_service.trg_validate(uid, 'pos.order', order.id, 'done', cr) - - return True - - def open_frontend_cb(self, cr, uid, ids, context=None): - if not context: - context = {} - if not ids: - return {} - context.update({'session_id' : ids[0]}) - return { - 'type' : 'ir.actions.client', - 'name' : 'Start Point Of Sale', - 'tag' : 'pos.ui', - 'context' : context, - } - -class pos_order(osv.osv): - _name = "pos.order" - _description = "Point of Sale" - _order = "id desc" - - def create_from_ui(self, cr, uid, orders, context=None): - #_logger.info("orders: %r", orders) - order_ids = [] - for tmp_order in orders: - order = tmp_order['data'] - order_id = self.create(cr, uid, { - 'name': order['name'], - 'user_id': order['user_id'] or False, - 'session_id': order['pos_session_id'], - 'lines': order['lines'] - }, context) - - for payments in order['statement_ids']: - payment = payments[2] - self.add_payment(cr, uid, order_id, { - 'amount': payment['amount'] or 0.0, - 'payment_date': payment['name'], - 'payment_name': payment.get('note', False), - 'journal': payment['journal_id'] - }, context=context) - - if order['amount_return']: - session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) - self.add_payment(cr, uid, order_id, { - 'amount': -order['amount_return'], - 'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'), - 'payment_name': _('return'), - 'journal': session.cash_journal_id.id - }, context=context) - order_ids.append(order_id) - wf_service = netsvc.LocalService("workflow") - wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) - return order_ids - - def unlink(self, cr, uid, ids, context=None): - for rec in self.browse(cr, uid, ids, context=context): - if rec.state not in ('draft','cancel'): - raise osv.except_osv(_('Unable to Delete !'), _('In order to delete a sale, it must be new or cancelled.')) - return super(pos_order, self).unlink(cr, uid, ids, context=context) - - def onchange_partner_id(self, cr, uid, ids, part=False, context=None): - if not part: - return {'value': {}} - pricelist = self.pool.get('res.partner').browse(cr, uid, part, context=context).property_product_pricelist.id - return {'value': {'pricelist_id': pricelist}} - - def _amount_all(self, cr, uid, ids, name, args, context=None): - tax_obj = self.pool.get('account.tax') - cur_obj = self.pool.get('res.currency') - res = {} - for order in self.browse(cr, uid, ids, context=context): - res[order.id] = { - 'amount_paid': 0.0, - 'amount_return':0.0, - 'amount_tax':0.0, - } - val1 = val2 = 0.0 - cur = order.pricelist_id.currency_id - for payment in order.statement_ids: - res[order.id]['amount_paid'] += payment.amount - res[order.id]['amount_return'] += (payment.amount < 0 and payment.amount or 0) - for line in order.lines: - val1 += line.price_subtotal_incl - val2 += line.price_subtotal - res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val1-val2) - res[order.id]['amount_total'] = cur_obj.round(cr, uid, cur, val1) - return res - - def copy(self, cr, uid, id, default=None, context=None): - if not default: - default = {} - d = { - 'state': 'draft', - 'invoice_id': False, - 'account_move': False, - 'picking_id': False, - 'statement_ids': [], - 'nb_print': 0, - 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order'), - } - d.update(default) - return super(pos_order, self).copy(cr, uid, id, d, context=context) - - _columns = { - 'name': fields.char('Order Ref', size=64, required=True, readonly=True), - 'company_id':fields.many2one('res.company', 'Company', required=True, readonly=True), - 'shop_id': fields.related('session_id', 'config_id', 'shop_id', relation='sale.shop', type='many2one', string='Shop', store=True, readonly=True), - 'date_order': fields.datetime('Order Date', readonly=True, select=True), - 'user_id': fields.many2one('res.users', 'Salesman', help="Person who uses the the cash register. It can be a reliever, a student or an interim employee."), - 'amount_tax': fields.function(_amount_all, string='Taxes', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), - 'amount_total': fields.function(_amount_all, string='Total', multi='all'), - 'amount_paid': fields.function(_amount_all, string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits_compute=dp.get_precision('Point Of Sale'), multi='all'), - 'amount_return': fields.function(_amount_all, 'Returned', digits_compute=dp.get_precision('Point Of Sale'), multi='all'), - 'lines': fields.one2many('pos.order.line', 'order_id', 'Order Lines', states={'draft': [('readonly', False)]}, readonly=True), - 'statement_ids': fields.one2many('account.bank.statement.line', 'pos_statement_id', 'Payments', states={'draft': [('readonly', False)]}, readonly=True), - 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, states={'draft': [('readonly', False)]}, readonly=True), - 'partner_id': fields.many2one('res.partner', 'Customer', change_default=True, select=1, states={'draft': [('readonly', False)], 'paid': [('readonly', False)]}), - - 'session_id' : fields.many2one('pos.session', 'Session', - #required=True, - select=1, - domain="[('state', '=', 'opened')]", - states={'draft' : [('readonly', False)]}, - readonly=True), - - 'state': fields.selection([('draft', 'New'), - ('cancel', 'Cancelled'), - ('paid', 'Paid'), - ('done', 'Posted'), - ('invoiced', 'Invoiced')], - 'Status', readonly=True), - - 'invoice_id': fields.many2one('account.invoice', 'Invoice'), - 'account_move': fields.many2one('account.move', 'Journal Entry', readonly=True), - 'picking_id': fields.many2one('stock.picking', 'Picking', readonly=True), - 'note': fields.text('Internal Notes'), - 'nb_print': fields.integer('Number of Print', readonly=True), - - 'sale_journal': fields.related('session_id', 'config_id', 'journal_id', relation='account.journal', type='many2one', string='Sale Journal', store=True, readonly=True), - } - - def _default_session(self, cr, uid, context=None): - so = self.pool.get('pos.session') - session_ids = so.search(cr, uid, [('state','=', 'opened'), ('user_id','=',uid)], context=context) - return session_ids and session_ids[0] or False - - def _default_pricelist(self, cr, uid, context=None): - res = self.pool.get('sale.shop').search(cr, uid, [], context=context) - if res: - shop = self.pool.get('sale.shop').browse(cr, uid, res[0], context=context) - return shop.pricelist_id and shop.pricelist_id.id or False - return False - - _defaults = { - 'user_id': lambda self, cr, uid, context: uid, - 'state': 'draft', - 'name': '/', - 'date_order': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), - 'nb_print': 0, - 'session_id': _default_session, - 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - 'pricelist_id': _default_pricelist, - } - - def create(self, cr, uid, values, context=None): - values['name'] = self.pool.get('ir.sequence').get(cr, uid, 'pos.order') - return super(pos_order, self).create(cr, uid, values, context=context) - - def test_paid(self, cr, uid, ids, context=None): - """A Point of Sale is paid when the sum - @return: True - """ - for order in self.browse(cr, uid, ids, context=context): - if order.lines and not order.amount_total: - return True - if (not order.lines) or (not order.statement_ids) or \ - (abs(order.amount_total-order.amount_paid) > 0.00001): - return False - return True - - def create_picking(self, cr, uid, ids, context=None): - """Create a picking for each order and validate it.""" - picking_obj = self.pool.get('stock.picking') - partner_obj = self.pool.get('res.partner') - move_obj = self.pool.get('stock.move') - - for order in self.browse(cr, uid, ids, context=context): - if not order.state=='draft': - continue - addr = order.partner_id and partner_obj.address_get(cr, uid, [order.partner_id.id], ['delivery']) or {} - picking_id = picking_obj.create(cr, uid, { - 'origin': order.name, - 'partner_id': addr.get('delivery',False), - 'type': 'out', - 'company_id': order.company_id.id, - 'move_type': 'direct', - 'note': order.note or "", - 'invoice_state': 'none', - 'auto_picking': True, - }, context=context) - self.write(cr, uid, [order.id], {'picking_id': picking_id}, context=context) - location_id = order.shop_id.warehouse_id.lot_stock_id.id - output_id = order.shop_id.warehouse_id.lot_output_id.id - - for line in order.lines: - if line.product_id and line.product_id.type == 'service': - continue - if line.qty < 0: - location_id, output_id = output_id, location_id - - move_obj.create(cr, uid, { - 'name': line.name, - 'product_uom': line.product_id.uom_id.id, - 'product_uos': line.product_id.uom_id.id, - 'picking_id': picking_id, - 'product_id': line.product_id.id, - 'product_uos_qty': abs(line.qty), - 'product_qty': abs(line.qty), - 'tracking_id': False, - 'state': 'draft', - 'location_id': location_id, - 'location_dest_id': output_id, - }, context=context) - if line.qty < 0: - location_id, output_id = output_id, location_id - - wf_service = netsvc.LocalService("workflow") - wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr) - picking_obj.force_assign(cr, uid, [picking_id], context) - return True - - def cancel_order(self, cr, uid, ids, context=None): - """ Changes order state to cancel - @return: True - """ - stock_picking_obj = self.pool.get('stock.picking') - for order in self.browse(cr, uid, ids, context=context): - wf_service.trg_validate(uid, 'stock.picking', order.picking_id.id, 'button_cancel', cr) - if stock_picking_obj.browse(cr, uid, order.picking_id.id, context=context).state <> 'cancel': - raise osv.except_osv(_('Error!'), _('Unable to cancel the picking.')) - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - return True - - def add_payment(self, cr, uid, order_id, data, context=None): - """Create a new payment for the order""" - if not context: - context = {} - statement_obj = self.pool.get('account.bank.statement') - statement_line_obj = self.pool.get('account.bank.statement.line') - prod_obj = self.pool.get('product.product') - property_obj = self.pool.get('ir.property') - curr_c = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id - curr_company = curr_c.id - order = self.browse(cr, uid, order_id, context=context) - args = { - 'amount': data['amount'], - } - if 'payment_date' in data: - args['date'] = data['payment_date'] - args['name'] = order.name - if data.get('payment_name', False): - args['name'] = args['name'] + ': ' + data['payment_name'] - account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context) - args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \ - and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False - args['partner_id'] = order.partner_id and order.partner_id.id or None - - if not args['account_id']: - if not args['partner_id']: - msg = _('There is no receivable account defined to make payment.') - else: - msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,) - raise osv.except_osv(_('Configuration Error!'), msg) - - context.pop('pos_session_id', False) - - try: - journal_id = long(data['journal']) - except Exception: - journal_id = False - - statement_id = False - for statement in order.session_id.statement_ids: - if statement.journal_id.id == journal_id: - statement_id = statement.id - break - - if not statement_id: - raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.')) - - args.update({ - 'statement_id' : statement_id, - 'pos_statement_id' : order_id, - 'journal_id' : journal_id, - 'type' : 'customer', - 'ref' : order.name, - }) - - statement_line_obj.create(cr, uid, args, context=context) - - wf_service = netsvc.LocalService("workflow") - wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) - wf_service.trg_write(uid, 'pos.order', order_id, cr) - - return statement_id - - def refund(self, cr, uid, ids, context=None): - """Create a copy of order for refund order""" - clone_list = [] - line_obj = self.pool.get('pos.order.line') - for order in self.browse(cr, uid, ids, context=context): - clone_id = self.copy(cr, uid, order.id, { - 'name': order.name + ' REFUND', - }, context=context) - clone_list.append(clone_id) - - for clone in self.browse(cr, uid, clone_list, context=context): - for order_line in clone.lines: - line_obj.write(cr, uid, [order_line.id], { - 'qty': -order_line.qty - }, context=context) - - new_order = ','.join(map(str,clone_list)) - abs = { - #'domain': "[('id', 'in', ["+new_order+"])]", - 'name': _('Return Products'), - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'pos.order', - 'res_id':clone_list[0], - 'view_id': False, - 'context':context, - 'type': 'ir.actions.act_window', - 'nodestroy': True, - 'target': 'current', - } - return abs - - def action_invoice_state(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'invoiced'}, context=context) - - def action_invoice(self, cr, uid, ids, context=None): - wf_service = netsvc.LocalService("workflow") - inv_ref = self.pool.get('account.invoice') - inv_line_ref = self.pool.get('account.invoice.line') - product_obj = self.pool.get('product.product') - inv_ids = [] - - for order in self.pool.get('pos.order').browse(cr, uid, ids, context=context): - if order.invoice_id: - inv_ids.append(order.invoice_id.id) - continue - - if not order.partner_id: - raise osv.except_osv(_('Error!'), _('Please provide a partner for the sale.')) - - acc = order.partner_id.property_account_receivable.id - inv = { - 'name': order.name, - 'origin': order.name, - 'account_id': acc, - 'journal_id': order.sale_journal.id or None, - 'type': 'out_invoice', - 'reference': order.name, - 'partner_id': order.partner_id.id, - 'comment': order.note or '', - 'currency_id': order.pricelist_id.currency_id.id, # considering partner's sale pricelist's currency - } - inv.update(inv_ref.onchange_partner_id(cr, uid, [], 'out_invoice', order.partner_id.id)['value']) - if not inv.get('account_id', None): - inv['account_id'] = acc - inv_id = inv_ref.create(cr, uid, inv, context=context) - - self.write(cr, uid, [order.id], {'invoice_id': inv_id, 'state': 'invoiced'}, context=context) - inv_ids.append(inv_id) - for line in order.lines: - inv_line = { - 'invoice_id': inv_id, - 'product_id': line.product_id.id, - 'quantity': line.qty, - } - inv_name = product_obj.name_get(cr, uid, [line.product_id.id], context=context)[0][1] - inv_line.update(inv_line_ref.product_id_change(cr, uid, [], - line.product_id.id, - line.product_id.uom_id.id, - line.qty, partner_id = order.partner_id.id, - fposition_id=order.partner_id.property_account_position.id)['value']) - if line.product_id.description_sale: - inv_line['note'] = line.product_id.description_sale - 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_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) - - if not inv_ids: return {} - - mod_obj = self.pool.get('ir.model.data') - res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') - res_id = res and res[1] or False - return { - 'name': _('Customer Invoice'), - 'view_type': 'form', - 'view_mode': 'form', - 'view_id': [res_id], - 'res_model': 'account.invoice', - 'context': "{'type':'out_invoice'}", - 'type': 'ir.actions.act_window', - 'nodestroy': True, - 'target': 'current', - 'res_id': inv_ids and inv_ids[0] or False, - } - - def create_account_move(self, cr, uid, ids, context=None): - return self._create_account_move_line(cr, uid, ids, None, None, context=context) - - def _create_account_move_line(self, cr, uid, ids, session=None, move_id=None, context=None): - # Tricky, via the workflow, we only have one id in the ids variable - """Create a account move line of order grouped by products or not.""" - account_move_obj = self.pool.get('account.move') - account_move_line_obj = self.pool.get('account.move.line') - account_period_obj = self.pool.get('account.period') - account_tax_obj = self.pool.get('account.tax') - user_proxy = self.pool.get('res.users') - property_obj = self.pool.get('ir.property') - - period = account_period_obj.find(cr, uid, context=context)[0] - - #session_ids = set(order.session_id for order in self.browse(cr, uid, ids, context=context)) - - if session and not all(session.id == order.session_id.id for order in self.browse(cr, uid, ids, context=context)): - raise osv.except_osv(_('Error!'), _('Selected orders do not have the same session!')) - - current_company = user_proxy.browse(cr, uid, uid, context=context).company_id - - grouped_data = {} - have_to_group_by = session and session.config_id.group_by or False - - def compute_tax(amount, tax, line): - if amount > 0: - tax_code_id = tax['base_code_id'] - tax_amount = line.price_subtotal * tax['base_sign'] - else: - tax_code_id = tax['ref_base_code_id'] - tax_amount = line.price_subtotal * tax['ref_base_sign'] - - return (tax_code_id, tax_amount,) - - for order in self.browse(cr, uid, ids, context=context): - if order.account_move: - continue - if order.state != 'paid': - continue - - user_company = user_proxy.browse(cr, order.user_id.id, order.user_id.id).company_id - - group_tax = {} - account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context).id - - order_account = order.partner_id and \ - order.partner_id.property_account_receivable and \ - order.partner_id.property_account_receivable.id or account_def or current_company.account_receivable.id - - if move_id is None: - # Create an entry for the sale - move_id = account_move_obj.create(cr, uid, { - 'ref' : order.name, - 'journal_id': order.sale_journal.id, - }, context=context) - - def insert_data(data_type, values): - # if have_to_group_by: - - sale_journal_id = order.sale_journal.id - - # 'quantity': line.qty, - # 'product_id': line.product_id.id, - values.update({ - 'date': order.date_order[:10], - 'ref': order.name, - 'journal_id' : sale_journal_id, - 'period_id' : period, - 'move_id' : move_id, - 'company_id': user_company and user_company.id or False, - }) - - if data_type == 'product': - key = ('product', values['product_id'],) - elif data_type == 'tax': - key = ('tax', values['tax_code_id'],) - elif data_type == 'counter_part': - key = ('counter_part', values['partner_id'], values['account_id']) - else: - return - - grouped_data.setdefault(key, []) - - # if not have_to_group_by or (not grouped_data[key]): - # grouped_data[key].append(values) - # else: - # pass - - if have_to_group_by: - if not grouped_data[key]: - grouped_data[key].append(values) - else: - current_value = grouped_data[key][0] - current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) - current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) - current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) - current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) - else: - grouped_data[key].append(values) - - # Create an move for each order line - - for line in order.lines: - tax_amount = 0 - taxes = [t for t in line.product_id.taxes_id] - computed_taxes = account_tax_obj.compute_all(cr, uid, taxes, line.price_unit * (100.0-line.discount) / 100.0, line.qty)['taxes'] - - for tax in computed_taxes: - tax_amount += round(tax['amount'], 2) - group_key = (tax['tax_code_id'], tax['base_code_id'], tax['account_collected_id'], tax['id']) - - group_tax.setdefault(group_key, 0) - group_tax[group_key] += round(tax['amount'], 2) - - amount = line.price_subtotal - - # Search for the income account - if line.product_id.property_account_income.id: - income_account = line.product_id.property_account_income.id - elif line.product_id.categ_id.property_account_income_categ.id: - income_account = line.product_id.categ_id.property_account_income_categ.id - else: - raise osv.except_osv(_('Error!'), _('Please define income '\ - 'account for this product: "%s" (id:%d).') \ - % (line.product_id.name, line.product_id.id, )) - - # Empty the tax list as long as there is no tax code: - tax_code_id = False - tax_amount = 0 - while computed_taxes: - tax = computed_taxes.pop(0) - tax_code_id, tax_amount = compute_tax(amount, tax, line) - - # If there is one we stop - if tax_code_id: - break - - # Create a move for the line - insert_data('product', { - 'name': line.product_id.name, - 'quantity': line.qty, - 'product_id': line.product_id.id, - 'account_id': income_account, - 'credit': ((amount>0) and amount) or 0.0, - 'debit': ((amount<0) and -amount) or 0.0, - 'tax_code_id': tax_code_id, - 'tax_amount': tax_amount, - 'partner_id': order.partner_id and order.partner_id.id or False - }) - - # For each remaining tax with a code, whe create a move line - for tax in computed_taxes: - tax_code_id, tax_amount = compute_tax(amount, tax, line) - if not tax_code_id: - continue - - insert_data('tax', { - 'name': _('Tax'), - 'product_id':line.product_id.id, - 'quantity': line.qty, - 'account_id': income_account, - 'credit': 0.0, - 'debit': 0.0, - 'tax_code_id': tax_code_id, - 'tax_amount': tax_amount, - }) - - # Create a move for each tax group - (tax_code_pos, base_code_pos, account_pos, tax_id)= (0, 1, 2, 3) - - for key, tax_amount in group_tax.items(): - tax = self.pool.get('account.tax').browse(cr, uid, key[tax_id], context=context) - insert_data('tax', { - 'name': _('Tax') + ' ' + tax.name, - 'quantity': line.qty, - 'product_id': line.product_id.id, - 'account_id': key[account_pos], - 'credit': ((tax_amount>0) and tax_amount) or 0.0, - 'debit': ((tax_amount<0) and -tax_amount) or 0.0, - 'tax_code_id': key[tax_code_pos], - 'tax_amount': tax_amount, - }) - - # counterpart - insert_data('counter_part', { - 'name': _("Trade Receivables"), #order.name, - 'account_id': order_account, - 'credit': ((order.amount_total < 0) and -order.amount_total) or 0.0, - 'debit': ((order.amount_total > 0) and order.amount_total) or 0.0, - 'partner_id': order.partner_id and order.partner_id.id or False - }) - - order.write({'state':'done', 'account_move': move_id}) - - for group_key, group_data in grouped_data.iteritems(): - for value in group_data: - account_move_line_obj.create(cr, uid, value, context=context) - - return True - - def action_payment(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'payment'}, context=context) - - def action_paid(self, cr, uid, ids, context=None): - self.create_picking(cr, uid, ids, context=context) - self.write(cr, uid, ids, {'state': 'paid'}, context=context) - return True - - def action_cancel(self, cr, uid, ids, context=None): - self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - return True - - def action_done(self, cr, uid, ids, context=None): - self.create_account_move(cr, uid, ids, context=context) - return True - -class account_bank_statement(osv.osv): - _inherit = 'account.bank.statement' - _columns= { - 'user_id': fields.many2one('res.users', 'User', readonly=True), - } - _defaults = { - 'user_id': lambda self,cr,uid,c={}: uid - } -account_bank_statement() - -class account_bank_statement_line(osv.osv): - _inherit = 'account.bank.statement.line' - _columns= { - 'pos_statement_id': fields.many2one('pos.order', ondelete='cascade'), - } -account_bank_statement_line() - -class pos_order_line(osv.osv): - _name = "pos.order.line" - _description = "Lines of Point of Sale" - _rec_name = "product_id" - - def _amount_line_all(self, cr, uid, ids, field_names, arg, context=None): - res = dict([(i, {}) for i in ids]) - account_tax_obj = self.pool.get('account.tax') - cur_obj = self.pool.get('res.currency') - for line in self.browse(cr, uid, ids, context=context): - taxes = line.product_id.taxes_id - price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - taxes = account_tax_obj.compute_all(cr, uid, line.product_id.taxes_id, price, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) - - cur = line.order_id.pricelist_id.currency_id - res[line.id]['price_subtotal'] = cur_obj.round(cr, uid, cur, taxes['total']) - res[line.id]['price_subtotal_incl'] = cur_obj.round(cr, uid, cur, taxes['total_included']) - return res - - def onchange_product_id(self, cr, uid, ids, pricelist, product_id, qty=0, partner_id=False, context=None): - context = context or {} - if not product_id: - return {} - if not pricelist: - raise osv.except_osv(_('No Pricelist !'), - _('You have to select a pricelist in the sale form !\n' \ - 'Please set one before choosing a product.')) - - price = self.pool.get('product.pricelist').price_get(cr, uid, [pricelist], - product_id, qty or 1.0, partner_id)[pricelist] - - result = self.onchange_qty(cr, uid, ids, product_id, 0.0, qty, price, context=context) - result['value']['price_unit'] = price - return result - - def onchange_qty(self, cr, uid, ids, product, discount, qty, price_unit, context=None): - result = {} - if not product: - return result - account_tax_obj = self.pool.get('account.tax') - cur_obj = self.pool.get('res.currency') - - 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) - - result['price_subtotal'] = taxes['total'] - result['price_subtotal_incl'] = taxes['total_included'] - return {'value': result} - - _columns = { - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'name': fields.char('Line No', size=32, required=True), - 'notice': fields.char('Discount Notice', size=128), - 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True, change_default=True), - 'price_unit': fields.float(string='Unit Price', digits=(16, 2)), - 'qty': fields.float('Quantity', digits=(16, 2)), - 'price_subtotal': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal w/o Tax', store=True), - 'price_subtotal_incl': fields.function(_amount_line_all, multi='pos_order_line_amount', string='Subtotal', store=True), - 'discount': fields.float('Discount (%)', digits=(16, 2)), - 'order_id': fields.many2one('pos.order', 'Order Ref', ondelete='cascade'), - 'create_date': fields.datetime('Creation Date', readonly=True), - } - - _defaults = { - 'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').get(cr, uid, 'pos.order.line'), - 'qty': lambda *a: 1, - 'discount': lambda *a: 0.0, - 'company_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id, - } - - def copy_data(self, cr, uid, id, default=None, context=None): - if not default: - default = {} - default.update({ - 'name': self.pool.get('ir.sequence').get(cr, uid, 'pos.order.line') - }) - return super(pos_order_line, self).copy_data(cr, uid, id, default, context=context) - -class pos_category(osv.osv): - _name = 'pos.category' - _description = "Point of Sale Category" - _order = "sequence, name" - def _check_recursion(self, cr, uid, ids, context=None): - level = 100 - while len(ids): - cr.execute('select distinct parent_id from pos_category where id IN %s',(tuple(ids),)) - ids = filter(None, map(lambda x:x[0], cr.fetchall())) - if not level: - return False - level -= 1 - return True - - _constraints = [ - (_check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) - ] - - def name_get(self, cr, uid, ids, context=None): - if not len(ids): - return [] - reads = self.read(cr, uid, ids, ['name','parent_id'], context=context) - res = [] - for record in reads: - name = record['name'] - if record['parent_id']: - name = record['parent_id'][1]+' / '+name - res.append((record['id'], name)) - return res - - def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): - res = self.name_get(cr, uid, ids, context=context) - return dict(res) - - def _get_image(self, cr, uid, ids, name, args, context=None): - result = dict.fromkeys(ids, False) - for obj in self.browse(cr, uid, ids, context=context): - result[obj.id] = tools.image_get_resized_images(obj.image) - return result - - def _set_image(self, cr, uid, id, name, value, args, context=None): - return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) - - _columns = { - 'name': fields.char('Name', size=64, required=True, translate=True), - 'complete_name': fields.function(_name_get_fnc, type="char", string='Name'), - 'parent_id': fields.many2one('pos.category','Parent Category', select=True), - 'child_id': fields.one2many('pos.category', 'parent_id', string='Children Categories'), - 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of product categories."), - - # NOTE: there is no 'default image', because by default we don't show thumbnails for categories. However if we have a thumbnail - # for at least one category, then we display a default image on the other, so that the buttons have consistent styling. - # In this case, the default image is set by the js code. - - 'image': fields.binary("Image", - help="This field holds the image used for the category. "\ - "The image is base64 encoded, and PIL-supported. "\ - "It is limited to a 1024x1024 px image."), - 'image_medium': fields.function(_get_image, fnct_inv=_set_image, - string="Medium-sized image", type="binary", multi="_get_image", - store = { - 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), - }, - help="Medium-sized image of the category. It is automatically "\ - "resized as a 180x180 px image, with aspect ratio preserved. "\ - "Use this field in form views or some kanban views."), - 'image_small': fields.function(_get_image, fnct_inv=_set_image, - string="Smal-sized image", type="binary", multi="_get_image", - store = { - 'pos.category': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), - }, - help="Small-sized image of the category. It is automatically "\ - "resized as a 50x50 px image, with aspect ratio preserved. "\ - "Use this field anywhere a small image is required."), - } - -import io, StringIO - -class ean_wizard(osv.osv_memory): - _name = 'pos.ean_wizard' - _columns = { - 'ean13_pattern': fields.char('Ean13 Pattern', size=32, required=True, translate=True), - } - def sanitize_ean13(self, cr, uid, ids, context): - for r in self.browse(cr,uid,ids): - ean13 = openerp.addons.product.product.sanitize_ean13(r.ean13_pattern) - m = context.get('active_model') - m_id = context.get('active_id') - self.pool.get(m).write(cr,uid,[m_id],{'ean13':ean13}) - return { 'type' : 'ir.actions.act_window_close' } - -class product_product(osv.osv): - _inherit = 'product.product' - - - #def _get_small_image(self, cr, uid, ids, prop, unknow_none, context=None): - # result = {} - # for obj in self.browse(cr, uid, ids, context=context): - # if not obj.product_image: - # result[obj.id] = False - # continue - - # image_stream = io.BytesIO(obj.product_image.decode('base64')) - # img = Image.open(image_stream) - # img.thumbnail((120, 100), Image.ANTIALIAS) - # img_stream = StringIO.StringIO() - # img.save(img_stream, "JPEG") - # result[obj.id] = img_stream.getvalue().encode('base64') - # return result - - _columns = { - 'income_pdt': fields.boolean('Point of Sale Cash In', help="This is a product you can use to put cash into a statement for the point of sale backend."), - 'expense_pdt': fields.boolean('Point of Sale Cash Out', help="This is a product you can use to take cash from a statement for the point of sale backend, exemple: money lost, transfer to bank, etc."), - 'pos_categ_id': fields.many2one('pos.category','Point of Sale Category', - help="If you want to sell this product through the point of sale, select the category it belongs to."), - 'to_weight' : fields.boolean('To Weight', help="This category contains products that should be weighted, mainly used for the self-checkout interface"), - } - _defaults = { - 'to_weight' : False, - } - - def edit_ean(self, cr, uid, ids, context): - return { - 'name': "Edit Ean", - 'type': 'ir.actions.act_window', - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'pos.ean_wizard', - 'target' : 'new', - 'view_id': False, - 'context':context, - } - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/locadis/static/src/css/pos.css b/addons/locadis/static/src/css/pos.css deleted file mode 100644 index 075b4dccc27..00000000000 --- a/addons/locadis/static/src/css/pos.css +++ /dev/null @@ -1,6 +0,0 @@ -.point-of-sale .screen p{ - font-size: 24px; -} -.product-list-empty{ - text-align:center; -} diff --git a/addons/locadis/static/src/js/main.js b/addons/locadis/static/src/js/main.js deleted file mode 100644 index db7bd1f5ded..00000000000 --- a/addons/locadis/static/src/js/main.js +++ /dev/null @@ -1,215 +0,0 @@ -openerp.locadis = function(instance){ - - var module = instance.point_of_sale; - var QWeb = instance.web.qweb; - - module.PosModel = module.PosModel.extend({ - // all this copy paste just to load the product.dont_vidange field ... :( - load_server_data: function(){ - var self = this; - - var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) - .pipe(function(users){ - self.set('user',users[0]); - - return self.fetch('res.company', - [ - 'currency_id', - 'email', - 'website', - 'company_registry', - 'vat', - 'name', - 'phone', - 'partner_id', - ], - [['id','=',users[0].company_id[0]]]); - }).pipe(function(companies){ - self.set('company',companies[0]); - - return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]); - }).pipe(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]]]); - }).pipe(function(currencies){ - self.set('currency',currencies[0]); - - return self.fetch('product.uom', null, null); - }).pipe(function(units){ - self.set('units',units); - var units_by_id = {}; - for(var i = 0, len = units.length; i < len; i++){ - units_by_id[units[i].id] = units[i]; - } - self.set('units_by_id',units_by_id); - - return self.fetch('product.packaging', null, null); - }).pipe(function(packagings){ - self.set('product.packaging',packagings); - - return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]); - }).pipe(function(users){ - self.set('user_list',users); - - return self.fetch('account.tax', ['amount', 'price_include', 'type']); - }).pipe(function(taxes){ - self.set('taxes', taxes); - - return self.fetch( - 'pos.session', - ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'], - [['state', '=', 'opened'], ['user_id', '=', self.session.uid]] - ); - }).pipe(function(sessions){ - self.set('pos_session', sessions[0]); - - return self.fetch( - 'pos.config', - ['name','journal_ids','shop_id','journal_id', - 'iface_self_checkout', 'iface_led', 'iface_cashdrawer', - 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard', - 'iface_print_via_proxy','iface_cashdrawer','state','sequence_id','session_ids'], - [['id','=', self.get('pos_session').config_id[0]]] - ); - }).pipe(function(configs){ - var pos_config = configs[0]; - self.set('pos_config', pos_config); - self.iface_electronic_scale = !!pos_config.iface_electronic_scale; - self.iface_print_via_proxy = !!pos_config.iface_print_via_proxy; - self.iface_vkeyboard = !!pos_config.iface_vkeyboard; - self.iface_self_checkout = !!pos_config.iface_self_checkout; - self.iface_cashdrawer = !!pos_config.iface_cashdrawer; - - return self.fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]); - }).pipe(function(shops){ - self.set('shop',shops[0]); - - return self.fetch('pos.category', ['id','name','parent_id','child_id','image']) - }).pipe(function(categories){ - self.db.add_categories(categories); - - return self.fetch( - 'product.product', - ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', - 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', - 'description_sale', 'description','dont_vidange'], - [['pos_categ_id','!=', false],['sale_ok','=',true]], - {pricelist: self.get('shop').pricelist_id[0]} // context for price - ); - }).pipe(function(products){ - self.db.add_products(products); - - return self.fetch( - 'account.bank.statement', - ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], - [['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]] - ); - }).pipe(function(bank_statements){ - self.set('bank_statements', bank_statements); - - return self.fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]); - }).pipe(function(journals){ - self.set('journals',journals); - - // associate the bank statements with their journals. - var bank_statements = self.get('bank_statements'); - for(var i = 0, ilen = bank_statements.length; i < ilen; i++){ - for(var j = 0, jlen = journals.length; j < jlen; j++){ - if(bank_statements[i].journal_id[0] === journals[j].id){ - bank_statements[i].journal = journals[j]; - bank_statements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method; - } - } - } - self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))}); - }); - - - return loaded; - }, - }); - - module.Orderline = module.Orderline.extend({ - export_for_printing: function(){ - var json = this._super(); - json.dont_vidange = this.get_product().get('dont_vidange'); - }, - }); - - module.ProductListWidget = module.ProductListWidget.extend({ - template_empty: 'ProductEmptyListWidget', - get_category: function(){ - return this.getParent().product_categories_widget.category; - }, - renderElement: function(){ - var ss = this.pos_widget.screen_selector; - if(this.get_category().name === 'Root' && ss && ss.get_user_mode() !== 'cashier'){ - this.replaceElement(_.str.trim(QWeb.render(this.template_empty,{widget:this}))); - }else{ - this._super(); - } - }, - }); - - /* - module.BarcodeReader = module.BarcodeReader.extend({ - parse_ean: function(ean){ - var parse_result = { - type:'unknown', // - prefix:'', - ean:ean, - base_ean: ean, - id:'', - value: 0, - unit: 'none', - }; - - function match_prefix(prefix_list, type){ - for(var i = 0; i < prefix_list.length; i++){ - var prefix = prefix_list[i]; - if(ean.substring(0,prefix.length) === prefix){ - parse_result.prefix = prefix; - parse_result.type = type; - return true; - } - } - return false; - } - if(!this.check_ean(ean)){ - parse_result.type = 'error'; - }else if(match_prefix( ['275'], 'cashier')){ - parse_result.id = ean.substring(0,7); - }else if(match_prefix( ['278'], 'client')){ - parse_result.id = ean.substring(0,7); - }else if(match_prefix( ['24'], 'weight')){ - parse_result.id = ean.substring(0,8); - parse_result.value = Number(ean.substring(8,12))/100.0; - parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); - parse_result.unit = 'Kg'; - }else if(match_prefix( ['291','292','295'],'price')){ - parse_result.id = ean.substring(0,7); - parse_result.value = Number(ean.substring(7,12))/100.0; - parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); - parse_result.unit = 'euro'; - }else if(match_prefix( ['295'],'price')){ - parse_result.id = ean.substring(0,8); - parse_result.value = Number(ean.substring(8,12))/100.0; - parse_result.base_ean = this.sanitize_ean(ean.substring(0,8)); - parse_result.unit = 'euro'; - }else if(match_prefix(['980'], 'price')){ - // bons de vidanges, prix negatif. - parse_result.id = ean.substring(0,8); - parse_result.value = - Number(ean.substring(8,12))/100.0; - parse_result.base_ean = this.sanitize_ean(ean.substring(0,8)); - parse_result.unit = 'euro'; - }else{ - parse_result.type = 'unit'; - parse_result.prefix = ''; - parse_result.id = ean; - } - return parse_result; - }, - }); - */ -}; diff --git a/addons/locadis/static/src/xml/pos.xml b/addons/locadis/static/src/xml/pos.xml deleted file mode 100644 index ad983b96721..00000000000 --- a/addons/locadis/static/src/xml/pos.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - -
    -
    - -

    Please scan your products

    -
    -
    -
    -
    - - - - - -
  • - Vidange: -
  • -
    -
    -
    - -
    From 56197a71b116690bf3f5543e2b8fc9f3b832109b Mon Sep 17 00:00:00 2001 From: "Turkesh Patel (Open ERP)" Date: Wed, 12 Sep 2012 10:52:27 +0530 Subject: [PATCH 25/82] [IMP] remove rml_footer1 and rml_footer2 from demo data. bzr revid: tpa@tinyerp.com-20120912052227-2kfaifr0hilas2lk --- openerp/addons/base/base_data.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml index 2fa9910758d..4c429fe56ff 100644 --- a/openerp/addons/base/base_data.xml +++ b/openerp/addons/base/base_data.xml @@ -62,8 +62,6 @@ Your Company Your Company Slogan - Web: http://www.yourcompany.com - Tel: +33 1 49 51 23 94 - Bank Accounts... to be configured in menu Settings > Accounting/Invoicing. From d4f450a9a32576c6948ab518418ee51f59e80f6e Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 12 Sep 2012 12:44:13 +0530 Subject: [PATCH 26/82] [IMP]all : Improve settings wizard with capital letters at the start of each sentence bzr revid: mma@tinyerp.com-20120912071413-2ik1suywyrb2qtuv --- addons/account/res_config.py | 24 +++++++++---------- addons/base_setup/res_config.py | 16 ++++++------- addons/crm_claim/res_config.py | 2 +- addons/hr/res_config.py | 18 +++++++------- addons/knowledge/res_config.py | 8 +++---- addons/marketing/res_config.py | 6 ++--- addons/mrp/res_config.py | 16 ++++++------- addons/project/res_config.py | 18 +++++++------- addons/purchase/res_config.py | 28 +++++++++++----------- addons/sale/res_config.py | 42 ++++++++++++++++----------------- addons/stock/res_config.py | 24 +++++++++---------- 11 files changed, 101 insertions(+), 101 deletions(-) diff --git a/addons/account/res_config.py b/addons/account/res_config.py index 91f1068d166..c4141faaab0 100644 --- a/addons/account/res_config.py +++ b/addons/account/res_config.py @@ -51,8 +51,8 @@ class account_config_settings(osv.osv_memory): 'code_digits': fields.integer('# of Digits', help="No. of digits to use for account code"), 'tax_calculation_rounding_method': fields.related('company_id', 'tax_calculation_rounding_method', type='selection', selection=[ - ('round_per_line', 'Round per Line'), - ('round_globally', 'Round Globally'), + ('round_per_line', 'Round per line'), + ('round_globally', 'Round globally'), ], string='Tax calculation rounding method', help="If you select 'Round per Line' : for each tax, the tax amount will first be computed and rounded for each PO/SO/invoice line and then these rounded amounts will be summed, leading to the total amount for that tax. If you select 'Round Globally': for each tax, the tax amount will be computed for each PO/SO/invoice line, then these amounts will be summed and eventually this total tax amount will be rounded. If you sell with tax included, you should choose 'Round per line' because you certainly want the sum of your tax-included line subtotals to be equal to the total amount with taxes."), 'sale_tax': fields.many2one("account.tax.template", "Default sale tax"), @@ -72,40 +72,40 @@ class account_config_settings(osv.osv_memory): 'sale_refund_journal_id': fields.many2one('account.journal', 'Sale refund journal'), 'sale_refund_sequence_prefix': fields.related('sale_refund_journal_id', 'sequence_id', 'prefix', type='char', string='Credit note sequence'), 'sale_refund_sequence_next': fields.related('sale_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next credit note number'), - 'purchase_journal_id': fields.many2one('account.journal', 'Purchase Journal'), + 'purchase_journal_id': fields.many2one('account.journal', 'Purchase journal'), 'purchase_sequence_prefix': fields.related('purchase_journal_id', 'sequence_id', 'prefix', type='char', string='Supplier invoice sequence'), 'purchase_sequence_next': fields.related('purchase_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier invoice number'), 'purchase_refund_journal_id': fields.many2one('account.journal', 'Purchase refund journal'), 'purchase_refund_sequence_prefix': fields.related('purchase_refund_journal_id', 'sequence_id', 'prefix', type='char', string='Supplier credit note sequence'), 'purchase_refund_sequence_next': fields.related('purchase_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier credit note number'), - 'module_account_check_writing': fields.boolean('pay your suppliers by check', + 'module_account_check_writing': fields.boolean('Pay your suppliers by check', help="""This allows you to check writing and printing. This installs the module account_check_writing."""), - 'module_account_accountant': fields.boolean('full accounting features: journals, legal statements, chart of accounts, etc.', + 'module_account_accountant': fields.boolean('Full accounting features: journals, legal statements, chart of accounts, etc.', help="""If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of Accounts, ...)"""), - 'module_account_asset': fields.boolean('assets management', + 'module_account_asset': fields.boolean('Assets management', help="""This allows you to manage the assets owned by a company or a person. It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines. This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of Accounts, ...)"""), - 'module_account_budget': fields.boolean('budget management', + 'module_account_budget': fields.boolean('Budget management', help="""This allows accountants to manage analytic and crossovered budgets. Once the master budgets and the budgets are defined, the project managers can set the planned amount on each analytic account. This installs the module account_budget."""), - 'module_account_payment': fields.boolean('manage payment orders', + 'module_account_payment': fields.boolean('Manage payment orders', help="""This allows you to create and manage your payment orders, with purposes to * serve as base for an easy plug-in of various automated payment mechanisms, and * provide a more efficient way to manage invoice payments. This installs the module account_payment."""), - 'module_account_voucher': fields.boolean('manage customer payments', + 'module_account_voucher': fields.boolean('Manage customer payments', help="""This includes all the basic requirements of voucher entries for bank, cash, sales, purchase, expense, contra, etc. This installs the module account_voucher."""), - 'module_account_followup': fields.boolean('manage customer payment follow-ups', + 'module_account_followup': fields.boolean('Manage customer payment follow-ups', help="""This allows to automate letters for unpaid invoices, with multi-level recalls. This installs the module account_followup."""), - 'group_proforma_invoices': fields.boolean('allow pro-forma invoices', + 'group_proforma_invoices': fields.boolean('Allow pro-forma invoices', implied_group='account.group_proforma_invoices', help="Allows you to put invoices in pro-forma state."), 'default_sale_tax': fields.many2one('account.tax', 'Default sale tax', @@ -114,7 +114,7 @@ class account_config_settings(osv.osv_memory): help="This purchase tax will be assigned by default on new products."), 'decimal_precision': fields.integer('Decimal precision on journal entries', help="""As an example, a decimal precision of 2 will allow journal entries like: 9.99 EUR, whereas a decimal precision of 4 will allow journal entries like: 0.0231 EUR."""), - 'group_multi_currency': fields.boolean('allow multi currencies', + 'group_multi_currency': fields.boolean('Allow multi currencies', implied_group='base.group_multi_currency', help="Allows you multi currency environment"), } diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py index e1920fc05d9..0dc4a0f0794 100644 --- a/addons/base_setup/res_config.py +++ b/addons/base_setup/res_config.py @@ -25,16 +25,16 @@ class base_config_settings(osv.osv_memory): _name = 'base.config.settings' _inherit = 'res.config.settings' _columns = { - 'module_multi_company': fields.boolean('manage multiple companies', + 'module_multi_company': fields.boolean('Manage multiple companies', help="""Work in multi-company environments, with appropriate security access between companies. This installs the module multi_company."""), - 'module_share': fields.boolean('allow documents sharing', + 'module_share': fields.boolean('Allow documents sharing', help="""Share or embbed any screen of openerp."""), - 'module_portal': fields.boolean('activate the customer/supplier portal', + 'module_portal': fields.boolean('Activate the customer/supplier portal', help="""Give access your customers and suppliers to their documents."""), - 'module_auth_anonymous': fields.boolean('activate the public portal', + 'module_auth_anonymous': fields.boolean('Activate the public portal', help="""Enable the public part of openerp, openerp becomes a public website."""), - 'module_auth_oauth': fields.boolean('use external authentication providers, sign in with google, facebook, ...'), + 'module_auth_oauth': fields.boolean('Use external authentication providers, sign in with google, facebook, ...'), 'module_base_import': fields.boolean("Allow users to import data from CSV files"), } @@ -57,17 +57,17 @@ class sale_config_settings(osv.osv_memory): _name = 'sale.config.settings' _inherit = 'res.config.settings' _columns = { - 'module_web_linkedin': fields.boolean('get contacts automatically from LinkedIn', + 'module_web_linkedin': fields.boolean('Get contacts automatically from linkedIn', help="""When you create a new contact (person or company), you will be able to load all the data from LinkedIn (photos, address, etc)."""), 'module_crm': fields.boolean('CRM'), - 'module_plugin_thunderbird': fields.boolean('enable Thunderbird plugin', + 'module_plugin_thunderbird': fields.boolean('Enable Thunderbird plugin', help="""The plugin allows you archive email and its attachments to the selected OpenERP objects. You can select a partner, or a lead and attach the selected mail as a .eml file in the attachment of a selected record. You can create documents for CRM Lead, Partner from the selected emails. This installs the module plugin_thunderbird."""), - 'module_plugin_outlook': fields.boolean('enable Outlook plugin', + 'module_plugin_outlook': fields.boolean('Enable Outlook plugin', help="""The Outlook plugin allows you to select an object that you would like to add to your email and its attachments from MS Outlook. You can select a partner, or a lead object and archive a selected diff --git a/addons/crm_claim/res_config.py b/addons/crm_claim/res_config.py index cf260fcd163..92e474d6a46 100644 --- a/addons/crm_claim/res_config.py +++ b/addons/crm_claim/res_config.py @@ -26,7 +26,7 @@ class crm_claim_settings(osv.osv_memory): _inherit = ['sale.config.settings', 'fetchmail.config.settings'] _columns = { - 'fetchmail_claim': fields.boolean("create claims from incoming mails", + 'fetchmail_claim': fields.boolean("Create claims from incoming mails", fetchmail_model='crm.claim', fetchmail_name='Incoming Claims', help="""Allows you to configure your incoming mail server, and create claims from incoming emails."""), } diff --git a/addons/hr/res_config.py b/addons/hr/res_config.py index 411040f45b8..7ae28348045 100644 --- a/addons/hr/res_config.py +++ b/addons/hr/res_config.py @@ -26,23 +26,23 @@ class hr_config_settings(osv.osv_memory): _inherit = 'res.config.settings' _columns = { - 'module_hr_timesheet_sheet': fields.boolean('allow timesheets validation by managers', + 'module_hr_timesheet_sheet': fields.boolean('Allow timesheets validation by managers', help ="""This installs the module hr_timesheet_sheet."""), - 'module_hr_attendance': fields.boolean('track attendances', + 'module_hr_attendance': fields.boolean('Track attendances', help ="""This installs the module hr_attendance."""), - 'module_hr_timesheet': fields.boolean('manage timesheets', + 'module_hr_timesheet': fields.boolean('Manage timesheets', help ="""This installs the module hr_timesheet."""), - 'module_hr_holidays': fields.boolean('manage holidays, leaves and allocation requests', + 'module_hr_holidays': fields.boolean('Manage holidays, leaves and allocation requests', help ="""This installs the module hr_holidays."""), - 'module_hr_expense': fields.boolean('manage employees expenses', + 'module_hr_expense': fields.boolean('Manage employees expenses', help ="""This installs the module hr_expense."""), - 'module_hr_recruitment': fields.boolean('manage the recruitment process', + 'module_hr_recruitment': fields.boolean('Manage the recruitment process', help ="""This installs the module hr_recruitment."""), - 'module_hr_contract': fields.boolean('record contracts per employee', + 'module_hr_contract': fields.boolean('Record contracts per employee', help ="""This installs the module hr_contract."""), - 'module_hr_evaluation': fields.boolean('organize employees periodic evaluation', + 'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation', help ="""This installs the module hr_evaluation."""), - 'module_hr_payroll': fields.boolean('manage payroll', + 'module_hr_payroll': fields.boolean('Manage payroll', help ="""This installs the module hr_payroll."""), } diff --git a/addons/knowledge/res_config.py b/addons/knowledge/res_config.py index 7d9f161701b..012bd670ee3 100644 --- a/addons/knowledge/res_config.py +++ b/addons/knowledge/res_config.py @@ -25,16 +25,16 @@ class knowledge_config_settings(osv.osv_memory): _name = 'knowledge.config.settings' _inherit = 'res.config.settings' _columns = { - 'module_document_page': fields.boolean('create static web pages', + 'module_document_page': fields.boolean('Create static web pages', help="""This installs the module document_page."""), - 'module_document': fields.boolean('manage documents', + 'module_document': fields.boolean('Manage documents', help="""This is a complete document management system, with: user authentication, full document search (but pptx and docx are not supported), and a document dashboard. This installs the module document."""), - 'module_document_ftp': fields.boolean('share repositories (FTP)', + 'module_document_ftp': fields.boolean('Share repositories (FTP)', help="""Access your documents in OpenERP through an FTP interface. This installs the module document_ftp."""), - 'module_document_webdav': fields.boolean('share repositories (WebDAV)', + 'module_document_webdav': fields.boolean('Share repositories (WebDAV)', help="""Access your documents in OpenERP through WebDAV. This installs the module document_webdav."""), } diff --git a/addons/marketing/res_config.py b/addons/marketing/res_config.py index 7584f00e6b0..7cedb65a373 100644 --- a/addons/marketing/res_config.py +++ b/addons/marketing/res_config.py @@ -25,14 +25,14 @@ class marketing_config_settings(osv.osv_memory): _name = 'marketing.config.settings' _inherit = 'res.config.settings' _columns = { - 'module_marketing_campaign': fields.boolean('Marketing Campaigns', + 'module_marketing_campaign': fields.boolean('Marketing campaigns', help="""Provides leads automation through marketing campaigns. Campaigns can in fact be defined on any resource, not just CRM leads. This installs the module marketing_campaign."""), - 'module_marketing_campaign_crm_demo': fields.boolean('Demo Data for Marketing Campaigns', + 'module_marketing_campaign_crm_demo': fields.boolean('Demo data for marketing campaigns', help="""Installs demo data like leads, campaigns and segments for Marketing Campaigns. This installs the module marketing_campaign_crm_demo."""), - 'module_crm_profiling': fields.boolean('Track Customer Profile to Focus your Campaigns', + 'module_crm_profiling': fields.boolean('Track customer profile to focus your campaigns', help="""Allows users to perform segmentation within partners. This installs the module crm_profiling."""), } diff --git a/addons/mrp/res_config.py b/addons/mrp/res_config.py index 555010c2be1..3cf1cb87487 100644 --- a/addons/mrp/res_config.py +++ b/addons/mrp/res_config.py @@ -28,11 +28,11 @@ class mrp_config_settings(osv.osv_memory): _inherit = 'res.config.settings' _columns = { - 'module_stock_planning': fields.boolean('manage master production shedule', + 'module_stock_planning': fields.boolean('Manage master production shedule', help ="""This allows to create a manual procurement plan apart of the normal MRP scheduling, which works automatically based on minimum stock rules. This installs the module stock_planning."""), - 'module_mrp_repair': fields.boolean("manage repairs of products ", + 'module_mrp_repair': fields.boolean("Manage repairs of products ", help="""Allows to manage all product repairs. * Add/remove products in the reparation * Impact for stocks @@ -41,7 +41,7 @@ class mrp_config_settings(osv.osv_memory): * Repair quotation report * Notes for the technician and for the final customer. This installs the module mrp_repair."""), - 'module_mrp_operations': fields.boolean("allow detailed planning of work order", + 'module_mrp_operations': fields.boolean("Allow detailed planning of work order", help="""This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab). This installs the module mrp_operations."""), 'module_mrp_subproduct': fields.boolean("produce several products from one manufacturing order", @@ -49,26 +49,26 @@ class mrp_config_settings(osv.osv_memory): Without this module: A + B + C -> D. With this module: A + B + C -> D + E. This installs the module mrp_subproduct."""), - 'module_mrp_jit': fields.boolean("generate procurement in real time", + 'module_mrp_jit': fields.boolean("Generate procurement in real time", help="""This allows Just In Time computation of procurement orders. All procurement orders will be processed immediately, which could in some cases entail a small performance impact. This installs the module mrp_jit."""), - 'module_stock_no_autopicking': fields.boolean("manage manual picking to fulfill manufacturing orders ", + 'module_stock_no_autopicking': fields.boolean("Manage manual picking to fulfill manufacturing orders ", help="""This module allows an intermediate picking process to provide raw materials to production orders. For example to manage production made by your suppliers (sub-contracting). To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking" and put the location of the supplier in the routing of the assembly operation. This installs the module stock_no_autopicking."""), - 'group_mrp_routings': fields.boolean("manage routings and work orders ", + 'group_mrp_routings': fields.boolean("Manage routings and work orders ", implied_group='mrp.group_mrp_routings', help="""Routings allow you to create and manage the manufacturing operations that should be followed within your work centers in order to produce a product. They are attached to bills of materials that will define the required raw materials."""), - 'group_mrp_properties': fields.boolean("allow several bill of materials per products", + 'group_mrp_properties': fields.boolean("Allow several bill of materials per products", implied_group='product.group_mrp_properties', help="""The selection of the right Bill of Material to use will depend on the properties specified on the sale order and the Bill of Material."""), - 'module_product_manufacturer': fields.boolean("define manufacturers on products ", + 'module_product_manufacturer': fields.boolean("Define manufacturers on products ", help="""This allows you to define the following for a product: * Manufacturer * Manufacturer Product Name diff --git a/addons/project/res_config.py b/addons/project/res_config.py index fcb3ecc9e21..6642dbf485d 100644 --- a/addons/project/res_config.py +++ b/addons/project/res_config.py @@ -27,38 +27,38 @@ class project_configuration(osv.osv_memory): _inherit = 'res.config.settings' _columns = { - 'module_project_mrp': fields.boolean('generate tasks from sale orders', + 'module_project_mrp': fields.boolean('Generate tasks from sale orders', help ="""This feature automatically creates project tasks from service products in sale orders. More precisely, tasks are created for procurement lines with product of type 'Service', procurement method 'Make to Order', and supply method 'Produce'. This installs the module project_mrp."""), - 'module_pad': fields.boolean("use integrated collaborative note pads on task", + 'module_pad': fields.boolean("Use integrated collaborative note pads on task", help="""Lets the company customize which Pad installation should be used to link to new pads (by default, http://ietherpad.com/). This installs the module pad."""), - 'module_project_timesheet': fields.boolean("record timesheet lines per tasks", + 'module_project_timesheet': fields.boolean("Record timesheet lines per tasks", help="""This allows you to transfer the entries under tasks defined for Project Management to the timesheet line entries for particular date and user, with the effect of creating, editing and deleting either ways. This installs the module project_timesheet."""), - 'module_project_long_term': fields.boolean("manage resources planning on gantt view", + 'module_project_long_term': fields.boolean("Manage resources planning on gantt view", help="""A long term project management module that tracks planning, scheduling, and resource allocation. This installs the module project_long_term."""), - 'module_project_issue': fields.boolean("track issues and bugs", + 'module_project_issue': fields.boolean("Track issues and bugs", help="""Provides management of issues/bugs in projects. This installs the module project_issue."""), 'time_unit': fields.many2one('product.uom', 'Working time unit', required=True, help="""This will set the unit of measure used in projects and tasks."""), - 'module_project_issue_sheet': fields.boolean("invoice working time on issues", + 'module_project_issue_sheet': fields.boolean("Invoice working time on issues", help="""Provides timesheet support for the issues/bugs management in project. This installs the module project_issue_sheet."""), - 'group_tasks_work_on_tasks': fields.boolean("compute work activities on tasks", + 'group_tasks_work_on_tasks': fields.boolean("Compute work activities on tasks", implied_group='project.group_tasks_work_on_tasks', help="Allows you to compute work on tasks."), - 'group_time_work_estimation_tasks': fields.boolean("manage time estimation on tasks", + 'group_time_work_estimation_tasks': fields.boolean("Manage time estimation on tasks", implied_group='project.group_time_work_estimation_tasks', help="Allows you to compute Time Estimation on tasks."), - 'group_manage_delegation_task': fields.boolean("allow task delegation", + 'group_manage_delegation_task': fields.boolean("Allow task delegation", implied_group='project.group_delegate_task', help="Allows you to delegate tasks to other users."), } diff --git a/addons/purchase/res_config.py b/addons/purchase/res_config.py index 3d9c858cec5..10822de39c7 100644 --- a/addons/purchase/res_config.py +++ b/addons/purchase/res_config.py @@ -29,36 +29,36 @@ class purchase_config_settings(osv.osv_memory): _columns = { 'default_invoice_method': fields.selection( - [('manual', 'Based on Purchase Order Lines'), - ('picking', 'Based on Receptions'), - ('order', 'Pre-Generate Draft Invoices based on Purchase Orders'), - ], 'default invoicing control method', required=True, default_model='purchase.order'), - 'group_purchase_pricelist':fields.boolean("manage pricelist per supplier", + [('manual', 'Based on purchase order lines'), + ('picking', 'Based on receptions'), + ('order', 'Pre-Generate draft invoices based on purchase orders'), + ], 'Default invoicing control method', required=True, default_model='purchase.order'), + 'group_purchase_pricelist':fields.boolean("Manage pricelist per supplier", implied_group='product.group_purchase_pricelist', help="""Allows to manage different prices based on rules per category of Supplier. Example: 10% for retailers, promotion of 5 EUR on this product, etc."""), - 'group_uom':fields.boolean("manage different units of measure for products", + 'group_uom':fields.boolean("Manage different units of measure for products", implied_group='product.group_uom', help="""Allows you to select and maintain different units of measure for products."""), - 'group_costing_method':fields.boolean("compute product cost price based on average cost", + 'group_costing_method':fields.boolean("Compute product cost price based on average cost", implied_group='product.group_costing_method', help="""Allows you to compute product cost price based on average cost."""), - 'group_purchase_delivery_address': fields.boolean("allow a different address for incoming products and invoicings", + 'group_purchase_delivery_address': fields.boolean("Allow a different address for incoming products and invoicings", implied_group='purchase.group_delivery_invoice_address', help="Allows you to specify different delivery and invoice addresses on a purchase order."), - 'module_purchase_analytic_plans': fields.boolean('allow using multiple analytic accounts on the same order', + 'module_purchase_analytic_plans': fields.boolean('Allow using multiple analytic accounts on the same order', help ="""Allows the user to maintain several analysis plans. These let you split lines on a purchase order between several accounts and analytic plans. This installs the module purchase_analytic_plans."""), - 'module_warning': fields.boolean("alerts by products or supllier", + 'module_warning': fields.boolean("Alerts by products or supplier", help="""Allow to configure warnings on products and trigger them when a user wants to purchase a given product or a given supplier. Example: Product: this product is deprecated, do not purchase more than 5. Supplier: don't forget to ask for an express delivery."""), - 'module_purchase_double_validation': fields.boolean("force two levels of approvals", + 'module_purchase_double_validation': fields.boolean("Force two levels of approvals", help="""Provide a double validation mechanism for purchases exceeding minimum amount. This installs the module purchase_double_validation."""), - 'module_purchase_requisition': fields.boolean("manage purchase requisitions", + 'module_purchase_requisition': fields.boolean("Manage purchase requisitions", help="""Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products. You can configure per product if you directly do a Request for Quotation to one supplier or if you want a purchase requisition to negotiate with several suppliers."""), @@ -74,9 +74,9 @@ class purchase_config_settings(osv.osv_memory): class account_config_settings(osv.osv_memory): _inherit = 'account.config.settings' _columns = { - 'module_purchase_analytic_plans': fields.boolean('use multiple analytic accounts on orders', + 'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on orders', help="""This allows install module purchase_analytic_plans."""), - 'group_analytic_account_for_purchases': fields.boolean('analytic accounting for purchases', + 'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases', implied_group='purchase.group_analytic_accounting', help="Allows you to specify an analytic account on purchase orders."), } diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py index 1e7b5b67c53..d84bc3595f5 100644 --- a/addons/sale/res_config.py +++ b/addons/sale/res_config.py @@ -27,70 +27,70 @@ class sale_configuration(osv.osv_memory): _inherit = 'sale.config.settings' _columns = { - 'group_invoice_so_lines': fields.boolean('generate invoices based on the sale order', + 'group_invoice_so_lines': fields.boolean('Generate invoices based on the sale order', implied_group='sale.group_invoice_so_lines', help="To allow your salesman to make invoices for sale order lines using the menu 'Lines to Invoice'."), - 'group_invoice_deli_orders': fields.boolean('generate invoices after and based on delivery orders', + 'group_invoice_deli_orders': fields.boolean('Generate invoices after and based on delivery orders', implied_group='sale.group_invoice_deli_orders', help="To allow your salesman to make invoices for Delivery Orders using the menu 'Deliveries to Invoice'."), - 'task_work': fields.boolean("prepare invoices based on task's activities", + 'task_work': fields.boolean("Prepare invoices based on task's activities", help="""Lets you transfer the entries under tasks defined for Project Management to the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways and to automatically creates project tasks from procurement lines. This installs the modules project_timesheet and project_mrp."""), - 'timesheet': fields.boolean('prepare invoices based on timesheets', + 'timesheet': fields.boolean('Prepare invoices based on timesheets', help = """For modifying account analytic view to show important data to project manager of services companies. You can also view the report of account analytic summary user-wise as well as month wise. This installs the module account_analytic_analysis."""), - 'module_account_analytic_analysis': fields.boolean('use contracts management', + 'module_account_analytic_analysis': fields.boolean('Use contracts management', help = """Allows to define your customer contracts conditions: invoicing method (fixed price, on timesheet, advance invoice), the exact pricing (650€/day for a developer), the duration (one year support contract). You will be able to follow the progress of the contract and invoice automatically. It installs the account_analytic_analysis module."""), 'default_order_policy': fields.selection( - [('manual', 'Invoice Based on Sales Orders'), ('picking', 'Invoice Based on Deliveries')], + [('manual', 'Invoice based on sales orders'), ('picking', 'Invoice based on deliveries')], 'The default invoicing method is', default_model='sale.order', help="You can generate invoices based on sales orders or based on shippings."), - 'module_delivery': fields.boolean('allow adding shipping costs', + 'module_delivery': fields.boolean('Allow adding shipping costs', help ="""Allows you to add delivery methods in sale orders and delivery orders. You can define your own carrier and delivery grids for prices. This installs the module delivery."""), - 'time_unit': fields.many2one('product.uom', 'the default working time unit for services is'), - 'default_picking_policy' : fields.boolean("deliver all at once when all products are available.", + 'time_unit': fields.many2one('product.uom', 'The default working time unit for services is'), + 'default_picking_policy' : fields.boolean("Deliver all at once when all products are available.", help = "Sales order by default will be configured to deliver all products at once instead of delivering each product when it is available. This may have an impact on the shipping price."), - 'group_sale_pricelist':fields.boolean("use pricelists to adapt your price per customers", + 'group_sale_pricelist':fields.boolean("Use pricelists to adapt your price per customers", implied_group='product.group_sale_pricelist', help="""Allows to manage different prices based on rules per category of customers. Example: 10% for retailers, promotion of 5 EUR on this product, etc."""), - 'group_uom':fields.boolean("allow using different units of measures", + 'group_uom':fields.boolean("Allow using different units of measures", implied_group='product.group_uom', help="""Allows you to select and maintain different units of measure for products."""), - 'group_sale_delivery_address': fields.boolean("allow a different address for delivery and invoicing ", + 'group_sale_delivery_address': fields.boolean("Allow a different address for delivery and invoicing ", implied_group='sale.group_delivery_invoice_address', help="Allows you to specify different delivery and invoice addresses on a sale order."), - 'group_mrp_properties': fields.boolean('product properties on order lines', + 'group_mrp_properties': fields.boolean('Product properties on order lines', implied_group='sale.group_mrp_properties', help="Allows you to tag sale order lines with properties."), - 'group_discount_per_so_line': fields.boolean("allow setting a discount on the sale order lines", + 'group_discount_per_so_line': fields.boolean("Allow setting a discount on the sale order lines", implied_group='sale.group_discount_per_so_line', help="Allows you to apply some discount per sale order line."), - 'group_multiple_shops': fields.boolean("manage multiple shops", + 'group_multiple_shops': fields.boolean("Manage multiple shops", implied_group='stock.group_locations', help="This allows to configure and use multiple shops."), - 'module_warning': fields.boolean("allow configuring alerts by customer or products", + 'module_warning': fields.boolean("Allow configuring alerts by customer or products", help="""Allow to configure warnings on products and trigger them when a user wants to sale a given product or a given customer. Example: Product: this product is deprecated, do not purchase more than 5. Supplier: don't forget to ask for an express delivery."""), - 'module_sale_margin': fields.boolean("display margins on sales orders", + 'module_sale_margin': fields.boolean("Display margins on sales orders", help="""This adds the 'Margin' on sales order. This gives the profitability by calculating the difference between the Unit Price and Cost Price. This installs the module sale_margin."""), - 'module_sale_journal': fields.boolean("allow batch invoicing of delivery orders through journals", + 'module_sale_journal': fields.boolean("Allow batch invoicing of delivery orders through journals", help="""Allows you to categorize your sales and deliveries (picking lists) between different journals, and perform batch operations on journals. This installs the module sale_journal."""), - 'module_analytic_user_function': fields.boolean("one employee can have different roles per contract", + 'module_analytic_user_function': fields.boolean("One employee can have different roles per contract", help="""Allows you to define what is the default function of a specific user on a given account. This is mostly used when a user encodes his timesheet. The values are retrieved and the fields are auto-filled. But the possibility to change these values is still available. @@ -172,9 +172,9 @@ class sale_configuration(osv.osv_memory): class account_config_settings(osv.osv_memory): _inherit = 'account.config.settings' _columns = { - 'module_sale_analytic_plans': fields.boolean('several analytic accounts on sales', + 'module_sale_analytic_plans': fields.boolean('Several analytic accounts on sales', help="""This allows install module sale_analytic_plans."""), - 'group_analytic_account_for_sales': fields.boolean('analytic accounting for sales', + 'group_analytic_account_for_sales': fields.boolean('Analytic accounting for sales', implied_group='sale.group_analytic_accounting', help="Allows you to specify an analytic account on sale orders."), } diff --git a/addons/stock/res_config.py b/addons/stock/res_config.py index 2e7018f4409..53cd657dbfa 100644 --- a/addons/stock/res_config.py +++ b/addons/stock/res_config.py @@ -26,14 +26,14 @@ class stock_config_settings(osv.osv_memory): _inherit = 'res.config.settings' _columns = { - 'module_claim_from_delivery': fields.boolean("allow claim on deliveries", + 'module_claim_from_delivery': fields.boolean("Allow claim on deliveries", help="""Adds a Claim link to the delivery order. This installs the module claim_from_delivery."""), - 'module_stock_invoice_directly': fields.boolean("create and open the invoice when the user finish a delivery order", + 'module_stock_invoice_directly': fields.boolean("Create and open the invoice when the user finish a delivery order", help="""This allows to automatically launch the invoicing wizard if the delivery is to be invoiced when you send or deliver goods. This installs the module stock_invoice_directly."""), - 'module_product_expiry': fields.boolean("expiry date on lots", + 'module_product_expiry': fields.boolean("Expiry date on lots", help="""Track different dates on products and serial numbers. The following dates can be tracked: - end of life @@ -41,36 +41,36 @@ class stock_config_settings(osv.osv_memory): - removal date - alert date. This installs the module product_expiry."""), - 'module_stock_location': fields.boolean("create push/pull logistic rules", + 'module_stock_location': fields.boolean("Create push/pull logistic rules", help="""Provide push and pull inventory flows. Typical uses of this feature are: manage product manufacturing chains, manage default locations per product, define routes within your warehouse according to business needs, etc. This installs the module stock_location."""), - 'group_uom': fields.boolean("manage units of measure on products", + 'group_uom': fields.boolean("Manage units of measure on products", implied_group='product.group_uom', help="""Allows you to select and maintain different units of measure for products."""), - 'group_uos': fields.boolean("invoice products in a different unit of measure than the sale order", + 'group_uos': fields.boolean("Invoice products in a different unit of measure than the sale order", implied_group='product.group_uos', help="""Allows you to sell units of a product, but invoice based on a different unit of measure. For instance, you can sell pieces of meat that you invoice based on their weight."""), - 'group_stock_packaging': fields.boolean("allow to define several packaging methods on products", + 'group_stock_packaging': fields.boolean("Allow to define several packaging methods on products", implied_group='product.group_stock_packaging', help="""Allows you to create and manage your packaging dimensions and types you want to be maintained in your system."""), - 'group_stock_production_lot': fields.boolean("track serial number on products", + 'group_stock_production_lot': fields.boolean("Track serial number on products", implied_group='stock.group_production_lot', help="""This allows you to manage products by using serial numbers. When you select a lot, you can get the upstream or downstream traceability of the products contained in lot."""), - 'group_stock_tracking_lot': fields.boolean("track serial number on logistic units (pallets)", + 'group_stock_tracking_lot': fields.boolean("Track serial number on logistic units (pallets)", implied_group='stock.group_tracking_lot', help="""Allows you to get the upstream or downstream traceability of the products contained in lot."""), - 'group_stock_inventory_valuation': fields.boolean("generate accounting entries per stock movement", + 'group_stock_inventory_valuation': fields.boolean("Generate accounting entries per stock movement", implied_group='stock.group_inventory_valuation', help="""Allows to configure inventory valuations on products and product categories."""), - 'group_stock_multiple_locations': fields.boolean("manage multiple locations and warehouses", + 'group_stock_multiple_locations': fields.boolean("Manage multiple locations and warehouses", implied_group='stock.group_locations', help="""This allows to configure and use multiple stock locations and warehouses, instead of having a single default one."""), - 'group_product_variant': fields.boolean("support multiple variants per products ", + 'group_product_variant': fields.boolean("Support multiple variants per products ", implied_group='product.group_product_variant', help="""Allow to manage several variants per product. As an example, if you sell T-Shirts, for the same "Linux T-Shirt", you may have variants on sizes or colors; S, M, L, XL, XXL."""), 'decimal_precision': fields.integer('Decimal precision on weight', help="As an example, a decimal precision of 2 will allow weights like: 9.99 kg, whereas a decimal precision of 4 will allow weights like: 0.0231 kg."), From 5a9c2544ef0577d6dffb09c08cd298cc25ca0e6f Mon Sep 17 00:00:00 2001 From: "ajay javiya (OpenERP)" Date: Wed, 12 Sep 2012 16:12:14 +0530 Subject: [PATCH 27/82] [IMP]: Print workflow report bzr revid: aja@tinyerp.com-20120912104214-z34o1hyk2qt67jhu --- addons/web/controllers/main.py | 28 ++++++++++++++++++++++++++-- addons/web/static/src/js/views.js | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index b635f5abdcd..6c44455e543 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1937,9 +1937,33 @@ class Reports(View): # maybe we should take of what characters can appear in a file name? ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])), ('Content-Type', report_mimetype), - ('Content-Length', len(report))], - cookies={'fileToken': int(token)}) + ('Content-Length', len(report))],) + @openerpweb.httprequest + def print_workflow(self, req, action, token): + action = simplejson.loads(action) + report_srv = req.session.proxy("report") + action.update({'nested': True}) + report_id = report_srv.report( + req.session._db, req.session._uid, req.session._password, + 'workflow.instance.graph', action['id'], action) + report_struct = None + while True: + report_struct = report_srv.report_get( + req.session._db, req.session._uid, + req.session._password, report_id) + if report_struct["state"]: + break + time.sleep(self.POLLING_DELAY) + report = base64.b64decode(report_struct['result']) + file_name = 'workflow_%s_%d.%s'%(action['model'], action['id'], report_struct['format']) + return req.make_response(report, + headers=[ + ('Content-Disposition', 'attachment; filename="%s"'% (file_name)), + ('Content-Type', 'application/pdf'), + ('Content-Length', len(report))], + cookies={'fileToken': int(token)}) + class Import(View): _cp_path = "/web/import" diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index d54e0194f38..ea35c2732bd 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -837,6 +837,21 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ }); break; case 'print_workflow': + var self = this + if (current_view.get_selected_ids().length != 1) { + instance.web.dialog($("
    ").text(_t("You must choose only one record.")), { title: _t("Warning"), modal: true }); + evt.currentTarget.selectedIndex = 0; + return false; + } else { + instance.web.blockUI(); + var action={"id":current_view.get_selected_ids()[0], + "model":self.dataset.model} + self.session.get_file({ + url: '/web/report/print_workflow', + data: {action: JSON.stringify(action)}, + complete: instance.web.unblockUI + }); + } break; default: if (val) { From f9f78f75f09e2e5be4884e06cb395c80d1dc243d Mon Sep 17 00:00:00 2001 From: "ajay javiya (OpenERP)" Date: Wed, 12 Sep 2012 16:49:43 +0530 Subject: [PATCH 28/82] [Add]: Add Unwanted changes bzr revid: aja@tinyerp.com-20120912111943-3p8tsawxqr9xtn9a --- addons/web/controllers/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 6c44455e543..d96e6b2d401 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1937,7 +1937,8 @@ class Reports(View): # maybe we should take of what characters can appear in a file name? ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])), ('Content-Type', report_mimetype), - ('Content-Length', len(report))],) + ('Content-Length', len(report))], + cookies={'fileToken': int(token)}) @openerpweb.httprequest def print_workflow(self, req, action, token): From f9bb479192bcb0415f6b4d04d8ee3b4d639e562a Mon Sep 17 00:00:00 2001 From: "Mayur Maheshwari (OpenERP)" Date: Wed, 12 Sep 2012 17:07:49 +0530 Subject: [PATCH 29/82] [IMP]all : Improve sentence bzr revid: mma@tinyerp.com-20120912113749-xyibq3gbsapvz012 --- addons/crm/res_config.py | 6 +++--- addons/hr_recruitment/res_config.py | 4 ++-- addons/project_issue/res_config.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/crm/res_config.py b/addons/crm/res_config.py index 1d30abdc33c..bb07af1f456 100644 --- a/addons/crm/res_config.py +++ b/addons/crm/res_config.py @@ -26,10 +26,10 @@ class crm_configuration(osv.osv_memory): _inherit = ['sale.config.settings', 'fetchmail.config.settings'] _columns = { - 'fetchmail_lead': fields.boolean("create leads from incoming mails", + 'fetchmail_lead': fields.boolean("Create leads from incoming mails", fetchmail_model='crm.lead', fetchmail_name='Incoming Leads', help="""Allows you to configure your incoming mail server, and create leads from incoming emails."""), - 'module_crm_caldav': fields.boolean("applications with Caldav protocol", + 'module_crm_caldav': fields.boolean("Applications with Caldav protocol", help="""Use protocol caldav to synchronize meetings with other calendar applications (like Sunbird). This installs the module crm_caldav."""), 'module_import_sugarcrm': fields.boolean("SugarCRM", @@ -38,7 +38,7 @@ class crm_configuration(osv.osv_memory): 'module_import_google': fields.boolean("Google (Contacts and Calendar)", help="""Import google contact in partner address and add google calendar events details in Meeting. This installs the module import_google."""), - 'module_google_map': fields.boolean("add google maps on customers", + 'module_google_map': fields.boolean("Add google maps on customers", help="""Locate customers on Google Map. This installs the module google_map."""), 'group_fund_raising': fields.boolean("Manage Fund Raising", diff --git a/addons/hr_recruitment/res_config.py b/addons/hr_recruitment/res_config.py index bf247f303f1..d80b0b71727 100644 --- a/addons/hr_recruitment/res_config.py +++ b/addons/hr_recruitment/res_config.py @@ -26,10 +26,10 @@ class hr_applicant_settings(osv.osv_memory): _inherit = ['hr.config.settings', 'fetchmail.config.settings'] _columns = { - 'module_document_ftp': fields.boolean('allow the automatic indexation of resumes', + 'module_document_ftp': fields.boolean('Allow the automatic indexation of resumes', help="""Manage your CV's and motivation letter related to all applicants. This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)"""), - 'fetchmail_applicants': fields.boolean('create applicants from an incoming email account', + 'fetchmail_applicants': fields.boolean('Create applicants from an incoming email account', fetchmail_model='hr.applicant', fetchmail_name='Incoming HR Applications', help ="""Allow applicants to send their job application to an email address (jobs@mycompany.com), and create automatically application documents in the system."""), diff --git a/addons/project_issue/res_config.py b/addons/project_issue/res_config.py index 9fdbf502a62..f286f9cac25 100644 --- a/addons/project_issue/res_config.py +++ b/addons/project_issue/res_config.py @@ -26,7 +26,7 @@ class project_issue_settings(osv.osv_memory): _inherit = ['project.config.settings', 'fetchmail.config.settings'] _columns = { - 'fetchmail_issue': fields.boolean("create issues from an incoming email account ", + 'fetchmail_issue': fields.boolean("Create issues from an incoming email account ", fetchmail_model='project.issue', fetchmail_name='Incoming Issues', help="""Allows you to configure your incoming mail server, and create issues from incoming emails."""), } From 6d03677e4eb3bd116d09ede645e08f7472c4ae6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 14:01:44 +0200 Subject: [PATCH 30/82] [CLEAN] mail_mail: cleaned some forgotten spaces; fixed the commend for the notification field. bzr revid: tde@openerp.com-20120912120144-m440eafqjtccv3sp --- addons/mail/mail_mail.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 7b593c5b998..8d3f3ab6e01 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -30,6 +30,7 @@ from tools.translate import _ _logger = logging.getLogger(__name__) + class mail_mail(osv.Model): """ Model holding RFC2822 email messages to send. This model also provides facilities to queue and send new email messages. """ @@ -58,8 +59,8 @@ class mail_mail(osv.Model): 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"), # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification - # and during unlink() we will cascade delete the parent and its attachments - 'notification': fields.boolean('Is Notification') + # and during unlink() we will not cascade delete the parent and its attachments + 'notification': fields.boolean('Is Notification') } def _get_default_from(self, cr, uid, context=None): @@ -76,21 +77,21 @@ class mail_mail(osv.Model): def create(self, cr, uid, values, context=None): if 'notification' not in values and values.get('mail_message_id'): values['notification'] = True - return super(mail_mail,self).create(cr, uid, values, context=context) + return super(mail_mail, self).create(cr, uid, values, context=context) def unlink(self, cr, uid, ids, context=None): # cascade-delete the parent message for all mails that are not created for a notification - ids_to_cascade = self.search(cr, uid, [('notification','=',False),('id','in',ids)]) + ids_to_cascade = self.search(cr, uid, [('notification', '=', False), ('id', 'in', ids)]) parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)] - res = super(mail_mail,self).unlink(cr, uid, ids, context=context) + res = super(mail_mail, self).unlink(cr, uid, ids, context=context) self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context) return res def mark_outgoing(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'outgoing'}, context=context) + return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context) def cancel(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'cancel'}, context=context) + return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) def process_email_queue(self, cr, uid, ids=None, context=None): """Send immediately queued messages, committing after each @@ -127,7 +128,7 @@ class mail_mail(osv.Model): """Perform any post-processing necessary after sending ``mail`` successfully, including deleting it completely along with its attachment if the ``auto_delete`` flag of the mail was set. - Overridden by subclasses for extra post-processing behaviors. + Overridden by subclasses for extra post-processing behaviors. :param browse_record mail: the mail that was just sent :return: True @@ -189,15 +190,15 @@ class mail_mail(osv.Model): res = ir_mail_server.send_email(cr, uid, msg, mail_server_id=mail.mail_server_id.id, context=context) if res: - mail.write({'state':'sent', 'message_id': res}) + mail.write({'state': 'sent', 'message_id': res}) else: - mail.write({'state':'exception'}) + mail.write({'state': 'exception'}) mail.refresh() if mail.state == 'sent': self._postprocess_sent_message(cr, uid, mail, context=context) except Exception: _logger.exception('failed sending mail.mail %s', mail.id) - mail.write({'state':'exception'}) + mail.write({'state': 'exception'}) if auto_commit == True: cr.commit() From ac2608d3dd9ced30bba7abf6e0192440fdf36aab Mon Sep 17 00:00:00 2001 From: Jigar Amin - OpenERP Date: Wed, 12 Sep 2012 18:33:09 +0530 Subject: [PATCH 31/82] [FIX] Passing right datas and ids to print the report bzr revid: jam@tinyerp.com-20120912130309-3jr2fe2yl7hgllwx --- addons/web/controllers/main.py | 5 +++-- addons/web/static/src/js/views.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index d96e6b2d401..ac8ebfa198c 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1944,10 +1944,11 @@ class Reports(View): def print_workflow(self, req, action, token): action = simplejson.loads(action) report_srv = req.session.proxy("report") - action.update({'nested': True}) + ids = action.pop('ids') + action.update({'nested': True, 'id': ids[0]}) report_id = report_srv.report( req.session._db, req.session._uid, req.session._password, - 'workflow.instance.graph', action['id'], action) + 'workflow.instance.graph', ids, action) report_struct = None while True: report_struct = report_srv.report_get( diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index ea35c2732bd..272ec0219b0 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -844,7 +844,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ return false; } else { instance.web.blockUI(); - var action={"id":current_view.get_selected_ids()[0], + var action={"ids":current_view.get_selected_ids(), "model":self.dataset.model} self.session.get_file({ url: '/web/report/print_workflow', From 14ed53e196441985e6242b4bcad4f0ed411646a8 Mon Sep 17 00:00:00 2001 From: Jigar Amin - OpenERP Date: Wed, 12 Sep 2012 18:49:51 +0530 Subject: [PATCH 32/82] [FIX] Should not print workflow in non standard views bzr revid: jam@tinyerp.com-20120912131951-6virlw8v8a8mccov --- addons/web/static/src/js/views.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 272ec0219b0..d0a5b1841bb 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -837,8 +837,12 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ }); break; case 'print_workflow': - var self = this - if (current_view.get_selected_ids().length != 1) { + var self = this; + if (self.active_view !== "form" && self.active_view !== "list"){ + instance.web.dialog($("
    ").text(_t("Workflow can be printed from 'Form' or 'List' view only.")), { title: _t("Warning"), modal: true }); + evt.currentTarget.selectedIndex = 0; + return false; + }else if (current_view.get_selected_ids().length != 1) { instance.web.dialog($("
    ").text(_t("You must choose only one record.")), { title: _t("Warning"), modal: true }); evt.currentTarget.selectedIndex = 0; return false; From 828ed33de7c2690405a8350ce9452606117d7a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 15:35:22 +0200 Subject: [PATCH 33/82] [ADD] mail: added Invite wizard, allowing to add partners to the followers. bzr revid: tde@openerp.com-20120912133522-af5dvae1rmburfwy --- addons/mail/mail_message.py | 24 ++++++++++ addons/mail/wizard/__init__.py | 1 + addons/mail/wizard/invite.py | 56 ++++++++++++++++++++++ addons/mail/wizard/invite_view.xml | 29 +++++++++++ addons/mail/wizard/mail_compose_message.py | 20 +------- 5 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 addons/mail/wizard/invite.py create mode 100644 addons/mail/wizard/invite_view.xml diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index e4862ca9fb9..6c65c6b302c 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -25,6 +25,7 @@ import tools from email.header import decode_header from operator import itemgetter from osv import osv, fields +from tools.translate import _ _logger = logging.getLogger(__name__) @@ -341,3 +342,26 @@ class mail_message(osv.Model): default = {} default.update(message_id=False, headers=False) return super(mail_message, self).copy(cr, uid, id, default=default, context=context) + + #------------------------------------------------------ + # Tools + #------------------------------------------------------ + + def verify_partner_email(self, cr, uid, partner_ids, context=None): + """ Verify that selected partner_ids have an email_address defined. + Otherwise throw a warning. """ + partner_wo_email_lst = [] + for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context): + if not partner.email: + partner_wo_email_lst.append(partner) + if not partner_wo_email_lst: + return {} + warning_msg = _('The following partners chosen as recipients for the email have no email address linked :') + for partner in partner_wo_email_lst: + warning_msg += '\n- %s' % (partner.name) + return {'warning': { + 'title': _('Partners email addresses not found'), + 'message': warning_msg, + } + } + diff --git a/addons/mail/wizard/__init__.py b/addons/mail/wizard/__init__.py index 88b9581f68e..39a2d843354 100644 --- a/addons/mail/wizard/__init__.py +++ b/addons/mail/wizard/__init__.py @@ -19,6 +19,7 @@ # ############################################################################## +import invite import mail_compose_message # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py new file mode 100644 index 00000000000..71ae23304a8 --- /dev/null +++ b/addons/mail/wizard/invite.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2012-Today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# +############################################################################## + +from osv import osv +from osv import fields + + +class invite_wizard(osv.osv_memory): + """ Wizard to invite partners and make them followers. """ + _name = 'mail.wizard.invite' + _description = 'Invite wizard' + + _columns = { + 'res_model': fields.char('Related Document Model', size=128, + required=True, select=1, + help='Model of the followed resource'), + 'res_id': fields.integer('Related Document ID', select=1, + help='Id of the followed resource'), + 'partner_ids': fields.many2many('res.partner', string='Partners'), + 'message': fields.text('Message'), + } + + def onchange_partner_ids(self, cr, uid, ids, value, context=None): + """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The + basic purpose of this method is to check that destination partners + effectively have email addresses. Otherwise a warning is thrown. + """ + res = {'value': {}} + if not value or not value[0] or not value[0][0] == 6: + return + res.update(self.pool.get('mail.message').verify_partner_email(cr, uid, value[0][2], context=context)) + return res + + def add_followers(self, cr, uid, ids, context=None): + for wizard in self.browse(cr, uid, ids, context=context): + model_obj = self.pool.get(wizard.res_model) + model_obj.message_subscribe(cr, uid, [wizard.res_id], [p.id for p in wizard.partner_ids], context=context) + return {'type': 'ir.actions.act_window_close'} diff --git a/addons/mail/wizard/invite_view.xml b/addons/mail/wizard/invite_view.xml new file mode 100644 index 00000000000..b932f96dfe7 --- /dev/null +++ b/addons/mail/wizard/invite_view.xml @@ -0,0 +1,29 @@ + + + + + + + Add Followers + mail.wizard.invite + +
    + + + + + + +
    +
    +
    +
    +
    + +
    +
    diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 3153ccc2069..fa4ca090943 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -192,24 +192,6 @@ class mail_compose_message(osv.TransientModel): specific behavior. """ return {'value': {'content_subtype': value}} - def _verify_partner_email(self, cr, uid, partner_ids, context=None): - """ Verify that selected partner_ids have an email_address defined. - Otherwise throw a warning. """ - partner_wo_email_lst = [] - for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context): - if not partner.email: - partner_wo_email_lst.append(partner) - if not partner_wo_email_lst: - return {} - warning_msg = _('The following partners chosen as recipients for the email have no email address linked :') - for partner in partner_wo_email_lst: - warning_msg += '\n- %s' % (partner.name) - return {'warning': { - 'title': _('Partners email addresses not found'), - 'message': warning_msg, - } - } - def onchange_partner_ids(self, cr, uid, ids, value, context=None): """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The basic purpose of this method is to check that destination partners @@ -218,7 +200,7 @@ class mail_compose_message(osv.TransientModel): res = {'value': {}} if not value or not value[0] or not value[0][0] == 6: return - res.update(self._verify_partner_email(cr, uid, value[0][2], context=context)) + res.update(self.verify_partner_email(cr, uid, value[0][2], context=context)) return res def unlink(self, cr, uid, ids, context=None): From a126eb72b7894f4d38943a63d4a0d3948fcf3067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 15:36:27 +0200 Subject: [PATCH 34/82] [ADD] Chatter: added Invite button and its behavior. It calls the Invite wizard. Added a read_value in mail_followers, to reload the follower values after subscribing, unsubscribing or adding followers through the wizard. bzr revid: tde@openerp.com-20120912133627-pid0uazohsx37trt --- addons/mail/static/src/css/mail.css | 4 -- addons/mail/static/src/js/mail_followers.js | 57 ++++++++++++------- addons/mail/static/src/xml/mail_followers.xml | 3 +- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 815f0ec92c4..a92f6c52832 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -96,10 +96,6 @@ width: 120px; } -.openerp button.oe_mail_button_followers { - display: inline; -} - .openerp button.oe_mail_button_mouseout { color: white; background-color: #8a89ba; diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index e5d216ed3d2..c1ef6d2c844 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -31,17 +31,9 @@ openerp_mail_followers = function(session, mail) { }, start: function() { - var self = this; - // NB: all the widget should be modified to check the actual_mode property on view, not use - // any other method to know if the view is in create mode anymore + // use actual_mode property on view to know if the view is in create mode anymore this.view.on("change:actual_mode", this, this._check_visibility); this._check_visibility(); - this.$el.find('button.oe_mail_button_follow').click(function () { self.do_follow(); }) - .mouseover(function () { $(this).html('Follow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) - .mouseleave(function () { $(this).html('Not following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); - this.$el.find('button.oe_mail_button_unfollow').click(function () { self.do_unfollow(); }) - .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) - .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); this.reinit(); }, @@ -49,15 +41,41 @@ openerp_mail_followers = function(session, mail) { this.$el.toggle(this.view.get("actual_mode") !== "create"); }, - destroy: function () { - this._super.apply(this, arguments); - }, - reinit: function() { this.$el.find('button.oe_mail_button_follow').hide(); this.$el.find('button.oe_mail_button_unfollow').hide(); }, + bind_events: function() { + var self = this; + this.$el.find('button.oe_mail_button_follow').on('click', function () { self.do_follow(); }) + this.$el.find('button.oe_mail_button_unfollow').on('click', function () { self.do_unfollow(); }) + .mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); }) + .mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); }); + this.$el.find('button.oe_mail_button_invite').on('click', function(event) { + action = { + type: 'ir.actions.act_window', + res_model: 'mail.wizard.invite', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: { + 'default_res_model': self.view.dataset.model, + 'default_res_id': self.view.datarecord.id + }, + } + self.do_action(action, function() { self.read_value(); }); + }); + }, + + read_value: function() { + var self = this; + return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).pipe(function (results) { + return results[0].message_follower_ids; + }).pipe(this.proxy('set_value')); + }, + set_value: function(value_) { this.reinit(); if (! this.view.datarecord.id || @@ -65,11 +83,12 @@ openerp_mail_followers = function(session, mail) { this.$el.find('div.oe_mail_recthread_aside').hide(); return; } - return this.fetch_followers(value_); + this.bind_events(); + return this.fetch_followers(value_ || this.get_value()); }, fetch_followers: function (value_) { - return this.ds_follow.call('read', [value_ || this.get_value(), ['name', 'user_ids']]).then(this.proxy('display_followers')); + return this.ds_follow.call('read', [value_, ['name', 'user_ids']]).pipe(this.proxy('display_followers')); }, /** Display the followers, evaluate is_follower directly */ @@ -91,13 +110,13 @@ openerp_mail_followers = function(session, mail) { }, do_follow: function () { - var context = new session.web.CompoundContext(this.build_context(), {'read_back': true}); - return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('set_value')); + var context = new session.web.CompoundContext(this.build_context(), {}); + return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value')); }, do_unfollow: function () { - var context = new session.web.CompoundContext(this.build_context(), {'read_back': true}); - return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('set_value')); + var context = new session.web.CompoundContext(this.build_context(), {}); + return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value')); }, }); }; diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml index 39622f42abf..32ce34d48d1 100644 --- a/addons/mail/static/src/xml/mail_followers.xml +++ b/addons/mail/static/src/xml/mail_followers.xml @@ -7,7 +7,8 @@ -->
    - + +
    From 7392ef4283763dcebc00cd5154f909c58d0368bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 15:37:11 +0200 Subject: [PATCH 35/82] [CLEAN] mail_thread: no need th have its hack about returning the new value after subscribing or unsubscribing. bzr revid: tde@openerp.com-20120912133711-tlt4pqm1cejjq9pv --- addons/mail/mail_thread.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index c0d6ac90fe6..d798007275f 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -623,14 +623,8 @@ class mail_thread(osv.AbstractModel): return self.message_subscribe(cr, uid, ids, partner_ids, context=context) def message_subscribe(self, cr, uid, ids, partner_ids, context=None): - """ Add partners to the records followers. - :param partner_ids: a list of partner_ids to subscribe - :param return: new value of followers if read_back key in context - """ - self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) - if context and context.get('read_back'): - return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids] - return [] + """ Add partners to the records followers. """ + return self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None): """ Wrapper on message_subscribe, using users. If user_ids is not @@ -640,14 +634,8 @@ class mail_thread(osv.AbstractModel): return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context) def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None): - """ Remove partners from the records followers. - :param partner_ids: a list of partner_ids to unsubscribe - :param return: new value of followers if read_back key in context - """ - self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context) - if context and context.get('read_back'): - return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids] - return [] + """ Remove partners from the records followers. """ + return self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context) #------------------------------------------------------ # Thread state From 01803926245d4d8a81464d9af4559cfcbd77cb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 15:37:33 +0200 Subject: [PATCH 36/82] [ADD] mail: added wizard in __openerp__. bzr revid: tde@openerp.com-20120912133733-xh484b24txefyhjm --- addons/mail/__openerp__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index 99dd6bda6ac..e55baa9ed75 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -61,6 +61,7 @@ The main features of the module are: 'website': 'http://www.openerp.com', 'depends': ['base', 'base_tools', 'base_setup'], 'data': [ + 'wizard/invite_view.xml', 'wizard/mail_compose_message_view.xml', 'res_config_view.xml', 'mail_message_view.xml', From a16bf464f5f3492f26eb4958e109f10653b22051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 15:38:43 +0200 Subject: [PATCH 37/82] [IMP] mail: added a new behavior when sending notifications. Each notification can now be customized (subject, body); each partner to notify will receive a different ir_email. It is still the same mail_mail as a basis, but specific content can be send to partners (for example, portal links). Updated the tests as well.L bzr revid: tde@openerp.com-20120912133843-lltf7pzder386e2w --- addons/mail/mail_followers.py | 59 ++++++++++++----------- addons/mail/mail_mail.py | 85 +++++++++++++++++++++++----------- addons/mail/tests/test_mail.py | 57 ++++++++++++++++------- 3 files changed, 132 insertions(+), 69 deletions(-) diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index 2aba685c4fa..146dc4b257c 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -84,16 +84,44 @@ class mail_notification(osv.Model): notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context) return self.write(cr, uid, notif_ids, {'read': True}, context=context) + def get_partners_to_notify(self, cr, uid, partner_ids, message, context=None): + """ Return the list of partners to notify, based on their preferences. + + :param message: browse_record of a mail.message + """ + notify_pids = [] + for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context): + # 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 + # Partner does not want to receive any emails + if partner.notification_email_send == 'none': + continue + # Partner wants to receive only emails and comments + if partner.notification_email_send == 'comment' and message.type not in ('email', 'comment'): + continue + # Partner wants to receive only emails + if partner.notification_email_send == 'email' and message.type != 'email': + continue + notify_pids.append(partner.id) + return notify_pids + def notify(self, cr, uid, partner_ids, msg_id, context=None): """ Send by email the notification depending on the user preferences """ context = context or {} # mail_noemail (do not send email) or no partner_ids: do not send, return if context.get('mail_noemail') or not partner_ids: return True - - mail_mail = self.pool.get('mail.mail') msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context) + notify_partner_ids = self.get_partners_to_notify(cr, uid, partner_ids, msg, context=context) + if not notify_partner_ids: + return True + + mail_mail = self.pool.get('mail.mail') # add signature body_html = msg.body signature = msg.author_id and msg.author_id.user_ids[0].signature or '' @@ -107,27 +135,6 @@ class mail_notification(osv.Model): 'body_html': body_html, 'state': 'outgoing', } - - for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context): - # 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 - # Partner does not want to receive any emails - if partner.notification_email_send == 'none': - continue - # Partner wants to receive only emails and comments - if partner.notification_email_send == 'comment' and msg.type not in ('email', 'comment'): - continue - # Partner wants to receive only emails - if partner.notification_email_send == 'email' and msg.type != 'email': - continue - if partner.email not in mail_values['email_to']: - mail_values['email_to'].append(partner.email) - if mail_values['email_to']: - mail_values['email_to'] = ', '.join(mail_values['email_to']) - email_notif_id = mail_mail.create(cr, uid, mail_values, context=context) - mail_mail.send(cr, uid, [email_notif_id], context=context) - return True + mail_values['email_to'] = ', '.join(mail_values['email_to']) + email_notif_id = mail_mail.create(cr, uid, mail_values, context=context) + return mail_mail.send(cr, uid, [email_notif_id], notifier_ids=notify_partner_ids, context=context) diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 8d3f3ab6e01..1cb9efa98e1 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -137,14 +137,46 @@ class mail_mail(osv.Model): mail.unlink() return True - def _send_get_mail_subject(self, cr, uid, mail, force=False, context=None): + def _send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): """ if void and related document: ' posted on ' - :param mail: mail.mail browse_record """ + + :param force: force the 'Author posted'... subject + :param mail: mail.mail browse_record + :param partner: browse_record of the specific recipient partner + """ if force or (not mail.subject and mail.model and mail.res_id): return '%s posted on %s' % (mail.author_id.name, mail.record_name) return mail.subject - def send(self, cr, uid, ids, auto_commit=False, context=None): + def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None): + """ Return a specific ir_email body. The main purpose of this method + is to be inherited by Portal, to add a link for signing in, in + each notification email a partner receives. + + :param mail: mail.mail browse_record + :param partner: browse_record of the specific recipient partner + """ + return mail.body_html + + def _send_get_ir_email_dict(self, cr, uid, mail, partner=None, context=None): + """ Return a dictionary for specific ir_email values, depending on a + partner, or generic to the whole recipients given by mail.email_to. + + :param mail: mail.mail browse_record + :param partner: browse_record of the specific recipient partner + """ + body = self._send_get_mail_body(cr, uid, mail, partner=partner, context=context) + subject = self._send_get_mail_subject(cr, uid, mail, partner=partner, context=context) + body_alternative = tools.html2plaintext(body) + email_to = [partner.email] if partner else tools.email_split(mail.email_to) + return { + 'body': body, + 'body_alternative': body_alternative, + 'subject': subject, + 'email_to': email_to, + } + + def send(self, cr, uid, ids, auto_commit=False, notifier_ids=None, context=None): """ Sends the selected emails immediately, ignoring their current state (mails that have already been sent should not be passed unless they should actually be re-sent). @@ -160,35 +192,36 @@ class mail_mail(osv.Model): ir_mail_server = self.pool.get('ir.mail_server') for mail in self.browse(cr, uid, ids, context=context): try: - body = mail.body_html - subject = self._send_get_mail_subject(cr, uid, mail, context=context) - # handle attachments attachments = [] for attach in mail.attachment_ids: attachments.append((attach.datas_fname, base64.b64decode(attach.datas))) - - # use only sanitized html and set its plaintexted version as alternative - body_alternative = tools.html2plaintext(body) - content_subtype_alternative = 'plain' + # specific behavior to customize the send email for notified partners + ir_email_list = [] + if notifier_ids: + for partner in self.pool.get('res.partner').browse(cr, uid, notifier_ids, context=context): + ir_email_list.append(self._send_get_ir_email_dict(cr, uid, mail, partner=partner, context=context)) + else: + ir_email_list.append(self._send_get_ir_email_dict(cr, uid, mail, context=context)) # build an RFC2822 email.message.Message object and send it without queuing - msg = ir_mail_server.build_email( - email_from = mail.email_from, - email_to = tools.email_split(mail.email_to), - subject = subject, - body = body, - body_alternative = body_alternative, - email_cc = tools.email_split(mail.email_cc), - reply_to = mail.reply_to, - attachments = attachments, - message_id = mail.message_id, - references = mail.references, - object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), - subtype = 'html', - subtype_alternative = content_subtype_alternative) - res = ir_mail_server.send_email(cr, uid, msg, - mail_server_id=mail.mail_server_id.id, context=context) + for ir_email in ir_email_list: + msg = ir_mail_server.build_email( + email_from = mail.email_from, + email_to = ir_email.get('email_to'), + subject = ir_email.get('subject'), + body = ir_email.get('body'), + body_alternative = ir_email.get('body_alternative'), + email_cc = tools.email_split(mail.email_cc), + reply_to = mail.reply_to, + attachments = attachments, + message_id = mail.message_id, + references = mail.references, + object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), + subtype = 'html', + subtype_alternative = 'plain') + res = ir_mail_server.send_email(cr, uid, msg, + mail_server_id=mail.mail_server_id.id, context=context) if res: mail.write({'state': 'sent', 'message_id': res}) else: diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index f45745f31dc..24d7db67b54 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -19,6 +19,8 @@ # ############################################################################## +import tools + from openerp.tests import common from openerp.tools.html_sanitize import html_sanitize @@ -29,7 +31,7 @@ Received: by mail1.openerp.com (Postfix, from userid 10002) From: Sylvie Lelitre Subject: {subject} MIME-Version: 1.0 -Content-Type: multipart/alternative; +Content-Type: multipart/alternative; boundary="----=_Part_4200734_24778174.1344608186754" Date: Fri, 10 Aug 2012 14:16:26 +0000 Message-ID: <1198923581.41972151344608186760.JavaMail@agrolait.com> @@ -52,9 +54,9 @@ Content-Transfer-Encoding: quoted-printable =20 =20 - +

    Please call me as soon as possible this afternoon!

    - +

    --
    Sylvie

    @@ -88,10 +90,19 @@ class test_mail(common.TransactionCase): return True def _mock_build_email(self, *args, **kwargs): - self._build_email_args = args - self._build_email_kwargs = kwargs + self._build_email_args_list.append(args) + self._build_email_kwargs_list.append(kwargs) return self.build_email_real(*args, **kwargs) + def _init_mock_build_email(self): + self._build_email_args_list = [] + self._build_email_kwargs_list = [] + + def _mock_send_get_mail_body(self, *args, **kwargs): + # def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None) + body = tools.append_content_to_html(args[2].body_html, kwargs.get('partner').name if kwargs.get('partner') else 'No specific partner') + return body + def setUp(self): super(test_mail, self).setUp() self.ir_model = self.registry('ir.model') @@ -106,10 +117,14 @@ class test_mail(common.TransactionCase): self.res_partner = self.registry('res.partner') # Install mock SMTP gateway + self._init_mock_build_email() self.build_email_real = self.registry('ir.mail_server').build_email self.registry('ir.mail_server').build_email = self._mock_build_email self.registry('ir.mail_server').send_email = self._mock_smtp_gateway + # Mock _send_get_mail_body to test its functionality without other addons override + self.registry('mail.mail')._send_get_mail_body = self._mock_send_get_mail_body + # groups@.. will cause the creation of new mail groups self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model', '=', 'mail.group')])[0] self.mail_alias.create(self.cr, self.uid, {'alias_name': 'groups', @@ -153,7 +168,7 @@ class test_mail(common.TransactionCase): test_msg_id = '' mail_text = MAIL_TEMPLATE_PLAINTEXT.format(to='groups@example.com', subject='frogs', extra='', msg_id=test_msg_id) self.mail_thread.message_process(cr, uid, None, mail_text) - new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id','=',test_msg_id)])[0]) + new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0]) self.assertEqual(new_mail.body, '\n

    \nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
    \n', 'plaintext mail incorrectly parsed') @@ -274,18 +289,20 @@ class test_mail(common.TransactionCase): _attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')] # CASE1: post comment, body and subject specified + self._init_mock_build_email() msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment') message = self.mail_message.browse(cr, uid, msg_id) - sent_email = self._build_email_kwargs + sent_emails = self._build_email_kwargs_list # Test: notifications have been deleted self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id)]), 'mail.mail notifications should have been auto-deleted!') # Test: mail_message: subject is _subject, body is _body1 (no formatting done) self.assertEqual(message.subject, _subject, 'mail.message subject incorrect') self.assertEqual(message.body, _body1, 'mail.message body incorrect') - # Test: sent_email: email send by server: correct subject, body; body_alternative - self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect') - self.assertEqual(sent_email['body'], _mail_body1, 'sent_email body incorrect') - self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1, 'sent_email body_alternative is incorrect') + # Test: sent_email: email send by server: correct subject, body, body_alternative + for sent_email in sent_emails: + self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect') + self.assertEqual(sent_email['body'], _mail_body1 + '\n
    Bert Tartopoils
    \n', 'sent_email body incorrect') + self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils', 'sent_email body_alternative is incorrect') # Test: mail_message: partner_ids = group followers message_pids = set([partner.id for partner in message.partner_ids]) test_pids = set([p_a_id, p_b_id, p_c_id]) @@ -295,14 +312,16 @@ class test_mail(common.TransactionCase): notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)]) self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect') # Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer) - self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect') + for sent_email in sent_emails: + self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect') # CASE2: post an email with attachments, parent_id, partner_ids # TESTS: automatic subject, signature in body_html, attachments propagation + self._init_mock_build_email() msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email', partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments) message = self.mail_message.browse(cr, uid, msg_id2) - sent_email = self._build_email_kwargs + sent_emails = self._build_email_kwargs_list self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id2)]), 'mail.mail notifications should have been auto-deleted!') # Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id @@ -310,9 +329,11 @@ class test_mail(common.TransactionCase): self.assertEqual(message.body, html_sanitize(_body2), 'mail.message body incorrect') self.assertEqual(message.parent_id.id, msg_id, 'mail.message parent_id incorrect') # Test: sent_email: email send by server: correct subject, body, body_alternative - self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect') - self.assertEqual(sent_email['body'], _mail_body2, 'sent_email body incorrect') - self.assertEqual(sent_email['body_alternative'], _mail_bodyalt2, 'sent_email body_alternative incorrect') + self.assertEqual(len(sent_emails), 2, 'sent_email number of sent emails incorrect') + for sent_email in sent_emails: + self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect') + self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect') + self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect') # Test: mail_message: partner_ids = group followers message_pids = set([partner.id for partner in message.partner_ids]) test_pids = set([p_a_id, p_b_id, p_c_id, p_d_id]) @@ -322,7 +343,8 @@ class test_mail(common.TransactionCase): notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)]) self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect') # Test: sent_email: email_to should contain b@b, c@c, not a@a (writer) - self.assertEqual(set(sent_email['email_to']), set(['b@b', 'c@c']), 'sent_email email_to incorrect') + for sent_email in sent_emails: + self.assertTrue(set(sent_email['email_to']).issubset(set(['b@b', 'c@c'])), 'sent_email email_to incorrect') # Test: attachments for attach in message.attachment_ids: self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect') @@ -458,6 +480,7 @@ class test_mail(common.TransactionCase): # It will be updated as soon as we have fixed specs ! cr, uid = self.cr, self.uid group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) + def _compare_structures(struct1, struct2, n=0): # print '%scompare structure' % ('\t' * n) self.assertEqual(len(struct1), len(struct2), 'message_read structure number of childs incorrect') From 30fb693c20745bb2e656732d7a6893b800ae0351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 17:53:00 +0200 Subject: [PATCH 38/82] [IMP] Portal: override of mail_mail to add the signin link into footer of send notifications. bzr revid: tde@openerp.com-20120912155300-8ilg2g24yl7qsjfg --- addons/mail/mail_mail.py | 14 +++++------ addons/portal/__init__.py | 1 + addons/portal/mail_mail.py | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 addons/portal/mail_mail.py diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 1cb9efa98e1..cf86c39f273 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -137,7 +137,7 @@ class mail_mail(osv.Model): mail.unlink() return True - def _send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): + def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): """ if void and related document: ' posted on ' :param force: force the 'Author posted'... subject @@ -148,7 +148,7 @@ class mail_mail(osv.Model): return '%s posted on %s' % (mail.author_id.name, mail.record_name) return mail.subject - def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None): + def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): """ Return a specific ir_email body. The main purpose of this method is to be inherited by Portal, to add a link for signing in, in each notification email a partner receives. @@ -158,15 +158,15 @@ class mail_mail(osv.Model): """ return mail.body_html - def _send_get_ir_email_dict(self, cr, uid, mail, partner=None, context=None): + def send_get_ir_email_dict(self, cr, uid, mail, partner=None, context=None): """ Return a dictionary for specific ir_email values, depending on a partner, or generic to the whole recipients given by mail.email_to. :param mail: mail.mail browse_record :param partner: browse_record of the specific recipient partner """ - body = self._send_get_mail_body(cr, uid, mail, partner=partner, context=context) - subject = self._send_get_mail_subject(cr, uid, mail, partner=partner, context=context) + body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) + subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context) body_alternative = tools.html2plaintext(body) email_to = [partner.email] if partner else tools.email_split(mail.email_to) return { @@ -200,9 +200,9 @@ class mail_mail(osv.Model): ir_email_list = [] if notifier_ids: for partner in self.pool.get('res.partner').browse(cr, uid, notifier_ids, context=context): - ir_email_list.append(self._send_get_ir_email_dict(cr, uid, mail, partner=partner, context=context)) + ir_email_list.append(self.send_get_ir_email_dict(cr, uid, mail, partner=partner, context=context)) else: - ir_email_list.append(self._send_get_ir_email_dict(cr, uid, mail, context=context)) + ir_email_list.append(self.send_get_ir_email_dict(cr, uid, mail, context=context)) # build an RFC2822 email.message.Message object and send it without queuing for ir_email in ir_email_list: diff --git a/addons/portal/__init__.py b/addons/portal/__init__.py index 9c6cc59be7d..2bfc6df0d96 100644 --- a/addons/portal/__init__.py +++ b/addons/portal/__init__.py @@ -20,6 +20,7 @@ ############################################################################## import portal +import mail_mail import wizard diff --git a/addons/portal/mail_mail.py b/addons/portal/mail_mail.py new file mode 100644 index 00000000000..d7f4d05d5ac --- /dev/null +++ b/addons/portal/mail_mail.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2011 OpenERP S.A (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import tools +from osv import osv + + +class mail_mail_portal(osv.Model): + """ Update of mail_mail class, to add the signin URL to notifications. + """ + _name = 'mail.mail' + _inherit = ['mail.mail'] + + def _generate_portal_url(self, cr, uid, partner_id, portal_group_id, key, context=None): + """ . """ + base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context) + return base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (partner_id, portal_group_id, key) + + def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): + """ Return a specific ir_email body. The main purpose of this method + is to be inherited by Portal, to add a link for signing in, in + each notification email a partner receives. + + :param mail: mail.mail browse_record + :param partner: browse_record of the specific recipient partner + """ + if partner: + url = self._generate_portal_url(cr, uid, partner.id, 12, 1234, context=context) + body = tools.append_content_to_html(mail.body_html, url) + return body + else: + return super(mail_mail_portal, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context) From 918dc35da53232569e67caeb8256bb6e9f9608bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 17:53:13 +0200 Subject: [PATCH 39/82] [ADD] Portal: added skeleton files for tests. bzr revid: tde@openerp.com-20120912155313-xhx6hqyfr85t0ncw --- addons/portal/tests/__init__.py | 27 ++++++++++++++ addons/portal/tests/test_portal.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 addons/portal/tests/__init__.py create mode 100644 addons/portal/tests/test_portal.py diff --git a/addons/portal/tests/__init__.py b/addons/portal/tests/__init__.py new file mode 100644 index 00000000000..c4717f80ebf --- /dev/null +++ b/addons/portal/tests/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import test_portal + +checks = [ + test_portal, +] + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py new file mode 100644 index 00000000000..93b9d220149 --- /dev/null +++ b/addons/portal/tests/test_portal.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2012-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.tests import common + + +class test_portal(common.TransactionCase): + + def _mock_smtp_gateway(self, *args, **kwargs): + return True + + def _mock_build_email(self, *args, **kwargs): + self._build_email_args_list.append(args) + self._build_email_kwargs_list.append(kwargs) + return self.build_email_real(*args, **kwargs) + + def _init_mock_build_email(self): + self._build_email_args_list = [] + self._build_email_kwargs_list = [] + + def setUp(self): + super(test_portal, self).setUp() + self.ir_model = self.registry('ir.model') + self.mail_group = self.registry('mail.group') + self.mail_mail = self.registry('mail.mail') + self.res_users = self.registry('res.users') + self.res_partner = self.registry('res.partner') + + # Install mock SMTP gateway + self._init_mock_build_email() + self.build_email_real = self.registry('ir.mail_server').build_email + self.registry('ir.mail_server').build_email = self._mock_build_email + self.registry('ir.mail_server').send_email = self._mock_smtp_gateway + + # create a 'pigs' group that will be used through the various tests + self.group_pigs_id = self.mail_group.create(self.cr, self.uid, + {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) + + def test_00_mail_invite(self): + cr, uid = self.cr, self.uid + print 'cacaprout' From a5bed8994c913064e2d501de4667536a24c6cb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 12 Sep 2012 18:12:29 +0200 Subject: [PATCH 40/82] [IMP] point_of_sale: debug window bzr revid: fva@openerp.com-20120912161229-4lgcgt4xt2cdzceo --- addons/point_of_sale/static/src/css/pos.css | 75 +++++++++++++ addons/point_of_sale/static/src/js/devices.js | 99 +++++++++-------- addons/point_of_sale/static/src/js/models.js | 2 + addons/point_of_sale/static/src/js/screens.js | 13 +-- addons/point_of_sale/static/src/js/widgets.js | 102 +++++++++++++++--- addons/point_of_sale/static/src/xml/pos.xml | 49 +++++++++ 6 files changed, 270 insertions(+), 70 deletions(-) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index dbe79703329..a727986bb1c 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -1102,6 +1102,81 @@ .point-of-sale .pos-actionbar .button.rightalign{ float:right; } +/* ********* The Debug Widget ********* */ + +.point-of-sale .debug-widget{ + z-index:100000; + position: absolute; + right: 10px; + top: 10px; + width: 200px; + font-size: 10px; + + background: rgba(0,0,0,0.82); + color: white; + text-shadow: none; + padding-bottom: 10px; + box-shadow: 0px 3px 20px rgba(0,0,0,0.3); + cursor:move; +} +.point-of-sale .debug-widget .toggle{ + position: absolute; + font-size: 16px; + cursor:pointer; + top:0px; + right:0px; + padding:10px; + padding-right:15px; +} +.point-of-sale .debug-widget .content{ + overflow: hidden; +} +.point-of-sale .debug-widget h1{ + background:black; + padding-top: 10px; + padding-left: 10px; + margin-top:0; + margin-bottom:0; +} +.point-of-sale .debug-widget .category{ + background: black; + padding-left: 10px; + margin: 0px; + font-weight: bold; + padding-top:3px; + padding-bottom:3px; +} +.point-of-sale .debug-widget .button{ + padding: 5px; + padding-left: 15px; + display: block; + cursor:pointer; +} +.point-of-sale .debug-widget .button:hover{ + background: rgba(96,21,177,0.45); +} +.point-of-sale .debug-widget input{ + margin-left:10px; + margin-top:7px; +} +.point-of-sale .debug-widget .status{ + padding: 5px; + padding-left: 15px; + display: block; + cursor:default; +} +.point-of-sale .debug-widget .status.on{ + padding: 5px; + padding-left: 15px; + display: block; + background-color: green; +} +.point-of-sale .debug-widget .status.on{ + padding: 5px; + padding-left: 15px; + display: block; + background-color: #6cd11d; +} /* ********* The PopupWidgets ********* */ diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index 46d11938128..65a28b5366a 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -1,26 +1,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sale - var debug_devices = new (instance.web.Class.extend({ - active: false, - payment_status: 'waiting_for_payment', - weight: 0, - activate: function(){ - this.active = true; - }, - deactivate: function(){ - this.active = false; - }, - set_weight: function(weight){ this.activate(); this.weight = weight; }, - accept_payment: function(){ this.activate(); this.payment_status = 'payment_accepted'; }, - reject_payment: function(){ this.activate(); this.payment_status = 'payment_rejected'; }, - delay_payment: function(){ this.activate(); this.payment_status = 'waiting_for_payment'; }, - }))(); - - if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ - window.debug_devices = debug_devices; - } - // this object interfaces with the local proxy to communicate to the various hardware devices // connected to the Point of Sale. As the communication only goes from the POS to the proxy, // methods are used both to signal an event, and to fetch information. @@ -38,20 +18,34 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal this.connection = new instance.web.JsonRPC(); this.connection.setup(url); + + this.bypass_proxy = false; + this.notifications = {}; }, message : function(name,params,success_callback, error_callback){ success_callback = success_callback || function(){}; error_callback = error_callback || function(){}; - if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ console.log('PROXY:',name,params); } - if(!(debug_devices && debug_devices.active)){ - this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback); + var callbacks = this.notifications[name] || []; + for(var i = 0; i < callbacks.length; i++){ + callbacks[i](params); } + + this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback); + }, + + // this allows the client to be notified when a proxy call is made. The notification + // callback will be executed with the same arguments as the proxy call + add_notification: function(name, callback){ + if(!this.notifications[name]){ + this.notifications[name] = []; + } + this.notifications[name].push(callback); }, //a product has been scanned and recognized with success @@ -78,12 +72,12 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal //the client is starting to weight weighting_start: function(){ - this.weight = 0; - if(debug_devices){ - debug_devices.weigth = 0; + if(!this.weighting){ + this.weight = 0; + this.weighting = true; + this.bypass_proxy = false; + this.message('weighting_start'); } - this.weighting = true; - this.message('weighting_start'); }, //returns the weight on the scale. @@ -91,22 +85,29 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal // and a weighting_end() weighting_read_kg: function(){ var self = this; - if(debug_devices && debug_devices.active){ - return debug_devices.weight; + if(this.bypass_proxy){ + return this.weight; }else{ this.message('weighting_read_kg',{},function(weight){ - if(self.weighting){ + if(self.weighting && !self.bypass_proxy){ self.weight = weight; } }); - return self.weight; + return this.weight; } }, + // sets a custom weight, ignoring the proxy returned value until the next weighting_end + debug_set_weight: function(kg){ + this.bypass_proxy = true; + this.weight = kg; + }, + // the client has finished weighting products weighting_end: function(){ this.weight = 0; this.weighting = false; + this.bypass_proxy = false; this.message('weighting_end'); }, @@ -116,9 +117,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal payment_request: function(price, method, info){ this.paying = true; this.payment_status = 'waiting_for_payment'; - if(debug_devices){ - debug_devices.payment_status = 'waiting_for_payment'; - } this.message('payment_request',{'price':price,'method':method,'info':info}); }, @@ -127,18 +125,30 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal // returns 'waiting_for_payment' | 'payment_accepted' | 'payment_rejected' is_payment_accepted: function(){ var self = this; - if(debug_devices.active){ - return debug_devices.payment_status; + if(this.bypass_proxy){ + this.bypass_proxy = false; + return this.payment_status; }else{ this.message('is_payment_accepted', {}, function(payment_status){ if(self.paying){ self.payment_status = payment_status; } }); - return self.payment_status; + return this.payment_status; } }, + + // override what the proxy says and accept the payment + debug_accept_payment: function(){ + this.bypass_proxy = true; + this.payment_status = 'payment_accepted'; + }, + // override what the proxy says and reject the payment + debug_reject_payment: function(){ + this.bypass_proxy = true; + this.payment_status = 'payment_rejected'; + }, // the client cancels his payment payment_canceled: function(){ this.paying = false; @@ -241,18 +251,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal this.price_prefix_set = attributes.price_prefix_set || {'23':''}; this.cashier_prefix_set = attributes.cashier_prefix_set || {'041':''}; this.client_prefix_set = attributes.client_prefix_set || {'042':''}; - - if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ - var self = this; - window.simulate_ean = function(ean,strict){ - ean = ean.toString(); - if(!strict){ - ean = self.sanitize_ean(ean); - } - console.log('SIMULATE EAN: ',ean); - self.on_ean(ean); - } - } }, save_callbacks: function(){ @@ -356,7 +354,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal value: 0, unit: 'none', }; - console.log('ean',ean); function match_prefix(prefix_set, type){ for(prefix in prefix_set){ diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 49521377ccb..5fbba5088d4 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -24,6 +24,8 @@ 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 + // default attributes values. If null, it will be loaded below. this.set({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 3b580868e6b..d2c79f19928 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -560,21 +560,21 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa //we get the first cashregister marked as self-checkout var selfCheckoutRegisters = []; - for(var i = 0; i < this.pos.get('cashRegisters').models.length; i++){ - var cashregister = this.pos.get('cashRegisters').models[i]; + for(var i = 0; i < self.pos.get('cashRegisters').models.length; i++){ + var cashregister = self.pos.get('cashRegisters').models[i]; if(cashregister.self_checkout_payment_method){ selfCheckoutRegisters.push(cashregister); } } - var cashregister = selfCheckoutRegisters[0] || this.pos.get('cashRegisters').models[0]; + var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0]; currentOrder.addPaymentLine(cashregister); self.pos.push_order(currentOrder.exportAsJSON()) currentOrder.destroy(); self.pos.proxy.transaction_end(); self.pos_widget.screen_selector.set_current_screen(self.next_screen); }else if(payment === 'payment_rejected'){ - clearInterval(this.intervalID); + clearInterval(self.intervalID); //TODO show a tryagain thingie ? } },500); @@ -603,13 +603,14 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa show_numpad: false, show_leftpane: false, barcode_product_action: function(ean){ - self.pos.proxy.transaction_start(); + this.pos.proxy.transaction_start(); this._super(ean); }, barcode_client_action: function(ean){ - self.pos.proxy.transaction_start(); + this.pos.proxy.transaction_start(); this._super(ean); + $('.goodbye-message').hide(); this.pos_widget.screen_selector.show_popup('choose-receipt'); }, diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 2aaf9098329..c8e03ccff08 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -694,6 +694,91 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); + module.DebugWidget = module.PosBaseWidget.extend({ + template: "DebugWidget", + eans:{ + admin_badge: '0410300000004', + client_badge: '0420100000005', + invalid_ean: '1232456', + soda_33cl: '5449000000996', + oranges_kg: '2100002031410', + lemon_price: '2301000001560', + unknown_product: '9900000000004', + }, + events:[ + 'scan_item_success', + 'scan_item_error_unrecognized', + 'payment_request', + 'open_cashbox', + 'print_receipt', + 'print_pdf_invoice', + ], + minimized: false, + start: function(){ + var self = this; + + this.$el.draggable(); + this.$('.toggle').click(function(){ + var content = self.$('.content'); + var bg = self.$el; + if(!self.minimized){ + content.animate({'height':'0'},200); + }else{ + content.css({'height':'auto'}); + } + self.minimized = !self.minimized; + }); + this.$('.button.accept_payment').click(function(){ + self.pos.proxy.debug_accept_payment(); + }); + this.$('.button.reject_payment').click(function(){ + self.pos.proxy.debug_reject_payment(); + }); + this.$('.button.set_weight').click(function(){ + var kg = Number(self.$('input.weight').val()); + if(!Number.isNaN(kg)){ + self.pos.proxy.debug_set_weight(kg); + } + }); + this.$('.button.custom_ean').click(function(){ + var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0'); + self.$('input.ean').val(ean); + self.pos.barcode_reader.on_ean(ean); + }); + _.each(this.eans, function(ean, name){ + self.$('.button.'+name).click(function(){ + self.$('input.ean').val(ean); + self.pos.barcode_reader.on_ean(ean); + }); + }); + _.each(this.events, function(name){ + self.pos.proxy.add_notification(name,function(){ + console.log('Notification:',name); + self.$('.status.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); + self.$('.status.'+name).animate({'background-color':'black'},2000); + }); + }); + self.pos.proxy.add_notification('help_needed',function(){ + self.$('.status.help_needed').addClass('on'); + }); + self.pos.proxy.add_notification('help_canceled',function(){ + self.$('.status.help_needed').removeClass('on'); + }); + self.pos.proxy.add_notification('transaction_start',function(){ + self.$('.status.transaction').addClass('on'); + }); + self.pos.proxy.add_notification('transaction_end',function(){ + self.$('.status.transaction').removeClass('on'); + }); + self.pos.proxy.add_notification('weighting_start',function(){ + self.$('.status.weighting').addClass('on'); + }); + self.pos.proxy.add_notification('weighting_end',function(){ + self.$('.status.weighting').removeClass('on'); + }); + }, + }); + // The PosWidget is the main widget that contains all other widgets in the PointOfSale. // It is mainly composed of : // - a header, containing the list of orders @@ -718,14 +803,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.leftpane_width = '440px'; this.cashier_controls_visible = true; this.image_cache = new module.ImageCache(); // for faster products image display - - /* - //Epileptic mode - setInterval(function(){ - $('body').css({'-webkit-filter':'hue-rotate('+Math.random()*360+'deg)' }); - },100); - */ - }, start: function() { @@ -769,11 +846,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();}); self.$('.loader img').hide(); - if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){ - window.pos = self.pos; - window.pos_widget = self.pos_widget; - } - },function(){ // error when loading models data from the backend self.$('.loader img').hide(); return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id']) @@ -898,6 +970,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa default_mode: this.pos.iface_self_checkout ? 'client' : 'cashier', }); + if(this.pos.debug){ + this.debug_widget = new module.DebugWidget(this); + this.debug_widget.appendTo(this.$('#content')); + } }, changed_pending_operations: function () { diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index a0b129da2f9..7c8b18a9bc3 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -414,6 +414,55 @@
    + +
    +

    Debug Window

    +
    ▾
    +
    +

    Payment Terminal

    + +
      +
    • Accept Payment
    • +
    • Reject Payment
    • +
    +

    Electronic Scale

    +
      +
    • +
    • Set Weight
    • +
    + +

    Barcode Scanner

    +
      +
    • +
    • Custom Ean13
    • +
    • Admin Badge
    • +
    • Client Badge
    • +
    • Soda 33cl
    • +
    • 3.141Kg Oranges
    • +
    • 1.54€ Lemon
    • +
    • Unknown Product
    • +
    • Invalid Ean
    • +
    + +

    Hardware Status

    +
      +
    • Help needed
    • +
    • Weighting
    • +
    • In Transaction
    • +
    +

    Hardware Events

    +
      +
    • Scan Item Success
    • +
    • Scan Item Unrecognized
    • +
    • Payment Request
    • +
    • Open Cashbox
    • + + +
    +
    +
    +
    +
  • From a1dec2072a5c02cac601f0c1b1590ccb39fa124e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 18:13:49 +0200 Subject: [PATCH 41/82] [FIX] Fixed test mocking wrong method. bzr revid: tde@openerp.com-20120912161349-ya5jvplze9ke3rtr --- addons/mail/tests/test_mail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index f5a0fa9d592..39f69ff0a68 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -122,8 +122,8 @@ class test_mail(common.TransactionCase): self.registry('ir.mail_server').build_email = self._mock_build_email self.registry('ir.mail_server').send_email = self._mock_smtp_gateway - # Mock _send_get_mail_body to test its functionality without other addons override - self.registry('mail.mail')._send_get_mail_body = self._mock_send_get_mail_body + # Mock send_get_mail_body to test its functionality without other addons override + self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body # groups@.. will cause the creation of new mail groups self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model', '=', 'mail.group')])[0] From 9dfca35b19d4e2cbc3ddbc74e36a4b5b0703275f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Wed, 12 Sep 2012 18:26:53 +0200 Subject: [PATCH 42/82] [IMP] point_of_sale: put debug window code in the right place bzr revid: fva@openerp.com-20120912162653-fnbvqwg4l51lveal --- addons/point_of_sale/static/src/js/widgets.js | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index c8e03ccff08..0ae127e0b1b 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -666,34 +666,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa hide: function(){ this.$el.hide(); }, }); - -// ---------- Main Point of Sale Widget ---------- - - // this is used to notify the user that data is being synchronized on the network - module.SynchNotificationWidget = module.PosBaseWidget.extend({ - template: "SynchNotificationWidget", - init: function(parent, options){ - options = options || {}; - this._super(parent, options); - }, - renderElement: function() { - var self = this; - this._super(); - this.$('.oe_pos_synch-notification-button').click(function(){ - self.pos.flush(); - }); - }, - start: function(){ - var self = this; - this.pos.bind('change:nbr_pending_operations', function(){ - self.renderElement(); - }); - }, - get_nbr_pending: function(){ - return this.pos.get('nbr_pending_operations'); - }, - }); - + // The debug widget lets the user control and monitor the hardware and software status + // without the use of the proxy module.DebugWidget = module.PosBaseWidget.extend({ template: "DebugWidget", eans:{ @@ -779,6 +753,34 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa }, }); +// ---------- Main Point of Sale Widget ---------- + + // this is used to notify the user that data is being synchronized on the network + module.SynchNotificationWidget = module.PosBaseWidget.extend({ + template: "SynchNotificationWidget", + init: function(parent, options){ + options = options || {}; + this._super(parent, options); + }, + renderElement: function() { + var self = this; + this._super(); + this.$('.oe_pos_synch-notification-button').click(function(){ + self.pos.flush(); + }); + }, + start: function(){ + var self = this; + this.pos.bind('change:nbr_pending_operations', function(){ + self.renderElement(); + }); + }, + get_nbr_pending: function(){ + return this.pos.get('nbr_pending_operations'); + }, + }); + + // The PosWidget is the main widget that contains all other widgets in the PointOfSale. // It is mainly composed of : // - a header, containing the list of orders From e7a24e7350efc7f38ad32df604cfdb813cfe452f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 12 Sep 2012 18:36:18 +0200 Subject: [PATCH 43/82] [IMP] mail: invite wizard: added default message; added email send to the newly added followers. bzr revid: tde@openerp.com-20120912163618-rp67taqr24zp2yei --- addons/mail/wizard/invite.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py index 71ae23304a8..4644dc1e55b 100644 --- a/addons/mail/wizard/invite.py +++ b/addons/mail/wizard/invite.py @@ -21,6 +21,7 @@ from osv import osv from osv import fields +from tools.translate import _ class invite_wizard(osv.osv_memory): @@ -38,6 +39,13 @@ class invite_wizard(osv.osv_memory): 'message': fields.text('Message'), } + _defaults = { + 'message': lambda s, *a, **k: s._get_default_message(*a, **k), + } + + def _get_default_message(self, cr, uid, context=None): + return _('You have been invited to follow a new document.') + def onchange_partner_ids(self, cr, uid, ids, value, context=None): """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The basic purpose of this method is to check that destination partners @@ -52,5 +60,19 @@ class invite_wizard(osv.osv_memory): def add_followers(self, cr, uid, ids, context=None): for wizard in self.browse(cr, uid, ids, context=context): model_obj = self.pool.get(wizard.res_model) - model_obj.message_subscribe(cr, uid, [wizard.res_id], [p.id for p in wizard.partner_ids], context=context) + document = model_obj.browse(cr, uid, wizard.res_id, context=context) + + # filter partner_ids to get the new followers, to avoid sending email to already following partners + new_follower_ids = [p.id for p in wizard.partner_ids if p.id not in document.message_follower_ids] + model_obj.message_subscribe(cr, uid, [wizard.res_id], new_follower_ids, context=context) + + # send an email + if wizard.message: + for follower_id in new_follower_ids: + mail_mail = self.pool.get('mail.mail') + mail_id = mail_mail.create(cr, uid, { + 'body_html': '
    %s
    ' % wizard.message, + 'auto_delete': True, + }, context=context) + mail_mail.send(cr, uid, [mail_id], notifier_ids=[follower_id], context=context) return {'type': 'ir.actions.act_window_close'} From 13583b36d3cd94cf1687ddd82d2b4bfe7a216dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 13 Sep 2012 10:41:19 +0200 Subject: [PATCH 44/82] [FIX] mail, portal: fixed tests to remove mockups at tearDown. Otherwise, when using tests, no email could be send because of the mockups installed during tests. bzr revid: tde@openerp.com-20120913084119-6ligp3o864vtzlmz --- addons/mail/tests/test_mail.py | 22 +++++++++++++++------- addons/portal/tests/test_portal.py | 11 +++++++++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py index 39f69ff0a68..bf9520f0e68 100644 --- a/addons/mail/tests/test_mail.py +++ b/addons/mail/tests/test_mail.py @@ -89,15 +89,15 @@ class test_mail(common.TransactionCase): def _mock_smtp_gateway(self, *args, **kwargs): return True - def _mock_build_email(self, *args, **kwargs): - self._build_email_args_list.append(args) - self._build_email_kwargs_list.append(kwargs) - return self.build_email_real(*args, **kwargs) - def _init_mock_build_email(self): self._build_email_args_list = [] self._build_email_kwargs_list = [] + def _mock_build_email(self, *args, **kwargs): + self._build_email_args_list.append(args) + self._build_email_kwargs_list.append(kwargs) + return self._build_email(*args, **kwargs) + def _mock_send_get_mail_body(self, *args, **kwargs): # def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None) body = tools.append_content_to_html(args[2].body_html, kwargs.get('partner').name if kwargs.get('partner') else 'No specific partner') @@ -118,11 +118,12 @@ class test_mail(common.TransactionCase): # Install mock SMTP gateway self._init_mock_build_email() - self.build_email_real = self.registry('ir.mail_server').build_email + self._build_email = self.registry('ir.mail_server').build_email self.registry('ir.mail_server').build_email = self._mock_build_email + self._send_email = self.registry('ir.mail_server').send_email self.registry('ir.mail_server').send_email = self._mock_smtp_gateway - # Mock send_get_mail_body to test its functionality without other addons override + self._send_get_mail_body = self.registry('mail.mail').send_get_mail_body self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body # groups@.. will cause the creation of new mail groups @@ -133,6 +134,13 @@ class test_mail(common.TransactionCase): self.group_pigs_id = self.mail_group.create(self.cr, self.uid, {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) + def tearDown(self): + # Remove mocks + self.registry('ir.mail_server').build_email = self._build_email + self.registry('ir.mail_server').send_email = self._send_email + self.registry('mail.mail').send_get_mail_body = self._send_get_mail_body + super(test_mail, self).tearDown() + def test_00_message_process(self): cr, uid = self.cr, self.uid # Incoming mail creates a new mail_group "frogs" diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py index 93b9d220149..b98614de5e9 100644 --- a/addons/portal/tests/test_portal.py +++ b/addons/portal/tests/test_portal.py @@ -30,7 +30,7 @@ class test_portal(common.TransactionCase): def _mock_build_email(self, *args, **kwargs): self._build_email_args_list.append(args) self._build_email_kwargs_list.append(kwargs) - return self.build_email_real(*args, **kwargs) + return self._build_email(*args, **kwargs) def _init_mock_build_email(self): self._build_email_args_list = [] @@ -46,14 +46,21 @@ class test_portal(common.TransactionCase): # Install mock SMTP gateway self._init_mock_build_email() - self.build_email_real = self.registry('ir.mail_server').build_email + self._build_email = self.registry('ir.mail_server').build_email self.registry('ir.mail_server').build_email = self._mock_build_email + self._send_email = self.registry('ir.mail_server').send_email self.registry('ir.mail_server').send_email = self._mock_smtp_gateway # create a 'pigs' group that will be used through the various tests self.group_pigs_id = self.mail_group.create(self.cr, self.uid, {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) + def tearDown(self): + # Remove mocks + self.registry('ir.mail_server').build_email = self._build_email + self.registry('ir.mail_server').send_email = self._send_email + super(test_portal, self).tearDown() + def test_00_mail_invite(self): cr, uid = self.cr, self.uid print 'cacaprout' From 02668c368230b420159ec7c67e631bba2c6cbb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 13 Sep 2012 11:32:46 +0200 Subject: [PATCH 45/82] [IMP] mail_group: when deleting a mail_group, delete also the related menu entry. bzr revid: tde@openerp.com-20120913093246-kkxjqspupbq7j7z8 --- addons/mail/mail_group.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 175ce650f06..fe0cc3950ae 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -19,13 +19,11 @@ # ############################################################################## -import datetime as DT import openerp import openerp.tools as tools -from operator import itemgetter from osv import osv from osv import fields -from tools.translate import _ + class mail_group(osv.Model): """ A mail_group is a collection of users sharing messages in a discussion @@ -47,7 +45,7 @@ class mail_group(osv.Model): _columns = { 'description': fields.text('Description'), 'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"), - 'public': fields.selection([('public','Public'),('private','Private'),('groups','Selected Group Only')], 'Privacy', required=True, + 'public': fields.selection([('public', 'Public'), ('private', 'Private'), ('groups', 'Selected Group Only')], 'Privacy', required=True, help='This group is visible by non members. \ Invisible groups can add members through the invite button.'), 'group_public_id': fields.many2one('res.groups', string='Authorized Group'), @@ -126,14 +124,14 @@ class mail_group(osv.Model): search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search') params = { 'search_view_id': search_ref and search_ref[1] or False, - 'domain': [('model','=','mail.group'), ('res_id','=',mail_group_id)], + 'domain': [('model', '=', 'mail.group'), ('res_id', '=', mail_group_id)], 'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id}, 'res_model': 'mail.message', 'thread_level': 1, } cobj = self.pool.get('ir.actions.client') newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context) - self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context) + self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,' + str(newref), 'mail_group_id': mail_group_id}, context=context) mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context) @@ -142,9 +140,13 @@ class mail_group(osv.Model): return mail_group_id def unlink(self, cr, uid, ids, context=None): + groups = self.browse(cr, uid, ids, context=context) # Cascade-delete mail aliases as well, as they should not exist without the mail group. mail_alias = self.pool.get('mail.alias') - alias_ids = [group.alias_id.id for group in self.browse(cr, uid, ids, context=context) if group.alias_id] + alias_ids = [group.alias_id.id for group in groups if group.alias_id] + # Cascade-delete menu entries as well + self.pool.get('ir.ui.menu').unlink(cr, uid, [group.menu_id.id for group in groups if group.menu_id], context=context) + # Delete mail_group res = super(mail_group, self).unlink(cr, uid, ids, context=context) mail_alias.unlink(cr, uid, alias_ids, context=context) return res From b1839623476824bd0f2e848039e1ff845da59092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 13 Sep 2012 11:33:20 +0200 Subject: [PATCH 46/82] [IMP] mail_wizard_invite: updated default message. An override of default_get allows to have a more precise message. bzr revid: tde@openerp.com-20120913093320-mhwp43kkgoqd0h33 --- addons/mail/wizard/invite.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py index 4644dc1e55b..a63608d5a23 100644 --- a/addons/mail/wizard/invite.py +++ b/addons/mail/wizard/invite.py @@ -29,6 +29,16 @@ class invite_wizard(osv.osv_memory): _name = 'mail.wizard.invite' _description = 'Invite wizard' + def default_get(self, cr, uid, fields, context=None): + result = super(invite_wizard, self).default_get(cr, uid, fields, context=context) + if 'message' in fields and result.get('res_model') and result.get('res_id'): + document_name = self.pool.get(result.get('res_model')).name_get(cr, uid, [result.get('res_id')], context=context)[0][1] + message = _('You have been invited to follow %s.' % document_name) + result['message'] = message + elif 'message' in fields: + result['message'] = _('You have been invited to follow a new document.') + return result + _columns = { 'res_model': fields.char('Related Document Model', size=128, required=True, select=1, @@ -39,13 +49,6 @@ class invite_wizard(osv.osv_memory): 'message': fields.text('Message'), } - _defaults = { - 'message': lambda s, *a, **k: s._get_default_message(*a, **k), - } - - def _get_default_message(self, cr, uid, context=None): - return _('You have been invited to follow a new document.') - def onchange_partner_ids(self, cr, uid, ids, value, context=None): """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The basic purpose of this method is to check that destination partners From 5de520e4dad39b002ab6bbe18ecfc1d8bdd23e89 Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Thu, 13 Sep 2012 12:06:28 +0200 Subject: [PATCH 47/82] [FIX] remove previous erroneous bugfix bzr revid: abo@openerp.com-20120913100628-oojngjznaeu0txyb --- addons/account/account_view.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index d11a6bc88bd..75359b4f207 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -112,7 +112,7 @@
  • From fd4b83e8d14e841d37622ddb0be811aa07bd2562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 13 Sep 2012 13:36:55 +0200 Subject: [PATCH 52/82] [FIX] tools.image: added an image conversion before sharpening; otherwise the filter could crash. bzr revid: tde@openerp.com-20120913113655-t5o3wk3r0tkp203m --- openerp/tools/image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/tools/image.py b/openerp/tools/image.py index f673906f328..38997978326 100644 --- a/openerp/tools/image.py +++ b/openerp/tools/image.py @@ -66,8 +66,9 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file # check image size: do not create a thumbnail if avoiding smaller images if avoid_if_small and image.size[0] <= size[0] and image.size[1] <= size[1]: return base64_source - # create a thumbnail: will resize and keep ratios + # create a thumbnail: will resize and keep ratios, then sharpen for better looking result image.thumbnail(size, Image.ANTIALIAS) + image.convert('RGB') image = image.filter(ImageFilter.SHARPEN) # create a transparent image for background background = Image.new('RGBA', size, (255, 255, 255, 0)) From a5d604465175d0138e7003959d7a36101ed352a8 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 13 Sep 2012 13:44:03 +0200 Subject: [PATCH 53/82] [IMP] res.partner.bank: change _prepare_name_get() to make it usable by onchange methods bzr revid: rco@openerp.com-20120913114403-ny6h6vpijymj6o4s --- openerp/addons/base/res/res_bank.py | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/openerp/addons/base/res/res_bank.py b/openerp/addons/base/res/res_bank.py index 3bfbd822dae..8f5d8aa8131 100644 --- a/openerp/addons/base/res/res_bank.py +++ b/openerp/addons/base/res/res_bank.py @@ -172,37 +172,35 @@ class res_partner_bank(osv.osv): ('required', field.required)] return res - def _prepare_name_get(self, cr, uid, bank_type_obj, bank_obj, context=None): + def _prepare_name_get(self, cr, uid, bank_dicts, context=None): + """ Format the name of a res.partner.bank. + This function is designed to be inherited to add replacement fields. + :param bank_dicts: a list of res.partner.bank dicts, as returned by the method read() + :return: [(id, name), ...], as returned by the method name_get() """ - Format the name of a res.partner.bank. - This function is designed to be inherited to add replacement fields. - :param browse_record bank_type_obj: res.partner.bank.type object - :param browse_record bank_obj: res.partner.bank object - :rtype: str - :return: formatted name of a res.partner.bank record - """ - return bank_type_obj.format_layout % bank_obj._data[bank_obj.id] + # prepare a mapping {code: format_layout} for all bank types + bank_type_obj = self.pool.get('res.partner.bank.type') + bank_types = bank_type_obj.browse(cr, uid, bank_type_obj.search(cr, uid, []), context=context) + bank_code_format = dict((bt.code, bt.format_layout) for bt in bank_types) + + res = [] + for data in bank_dicts: + name = data['acc_number'] + if data['state'] and bank_code_format.get(data['state']): + try: + if not data.get('bank_name'): + data['bank_name'] = _('BANK') + name = bank_code_format[data['state']] % data + except Exception: + raise osv.except_osv(_("Formating Error"), _("Invalid Bank Account Type Name format.")) + res.append((data['id'], name)) + return res def name_get(self, cr, uid, ids, context=None): if not len(ids): return [] - bank_type_obj = self.pool.get('res.partner.bank.type') - res = [] - for val in self.browse(cr, uid, ids, context=context): - result = val.acc_number - if val.state: - type_ids = bank_type_obj.search(cr, uid, [('code','=',val.state)]) - if type_ids: - t = bank_type_obj.browse(cr, uid, type_ids[0], context=context) - try: - # avoid the default format_layout to result in "False: ..." - if not val._data[val.id]['bank_name']: - val._data[val.id]['bank_name'] = _('BANK') - result = self._prepare_name_get(cr, uid, t, val, context=context) - except Exception: - raise osv.except_osv(_("Formating Error"), _("Invalid Bank Account Type Name format.")) - res.append((val.id, result)) - return res + bank_dicts = self.read(cr, uid, ids, context=context) + return self._prepare_name_get(cr, uid, bank_dicts, context=context) def onchange_company_id(self, cr, uid, ids, company_id, context=None): result = {} From a44eb10af458b04b02f76e1a657f59280d78db13 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 13 Sep 2012 13:46:09 +0200 Subject: [PATCH 54/82] [IMP] account: adapt _prepare_name_get() of res.partner.bank to its new api bzr revid: rco@openerp.com-20120913114609-pff1x0spvb0gja2u --- addons/account/account_bank.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/addons/account/account_bank.py b/addons/account/account_bank.py index ef146128a67..e44d2acd163 100644 --- a/addons/account/account_bank.py +++ b/addons/account/account_bank.py @@ -43,11 +43,15 @@ class bank(osv.osv): "Return the name to use when creating a bank journal" return (bank.bank_name or '') + ' ' + bank.acc_number - def _prepare_name_get(self, cr, uid, bank_type_obj, bank_obj, context=None): - """Add ability to have %(currency_name)s in the format_layout of - res.partner.bank.type""" - bank_obj._data[bank_obj.id]['currency_name'] = bank_obj.currency_id and bank_obj.currency_id.name or '' - return super(bank, self)._prepare_name_get(cr, uid, bank_type_obj, bank_obj, context=context) + 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""" + currency_ids = list(set(data['currency_id'][0] for data in bank_dicts if data['currency_id'])) + currencies = self.pool.get('res.currency').browse(cr, uid, currency_ids, context=context) + currency_name = dict((currency.id, currency.name) for currency in currencies) + + for data in bank_dicts: + data['currency_name'] = data['currency_id'] and currency_name[data['currency_id'][0]] or '' + return super(bank, self)._prepare_name_get(cr, uid, bank_dicts, context=context) def post_write(self, cr, uid, ids, context={}): if isinstance(ids, (int, long)): From 0e4422809ba411a629b03baf705a3a7dd781302f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 13 Sep 2012 13:49:17 +0200 Subject: [PATCH 55/82] [IMP] email_template: now inherit from TestMailMockups test class. bzr revid: tde@openerp.com-20120913114917-na341w0ja7n0ditu --- addons/email_template/tests/test_mail.py | 16 ++-------------- addons/portal/tests/test_portal.py | 1 - 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py index 85ab3afc902..1091c34ad5c 100644 --- a/addons/email_template/tests/test_mail.py +++ b/addons/email_template/tests/test_mail.py @@ -20,17 +20,10 @@ ############################################################################## import base64 -from openerp.tests import common +from openerp.addons.mail.tests import test_mail -class test_message_compose(common.TransactionCase): - def _mock_smtp_gateway(self, *args, **kwargs): - return True - - def _mock_build_email(self, *args, **kwargs): - self._build_email_args = args - self._build_email_kwargs = kwargs - return self.build_email_real(*args, **kwargs) +class test_message_compose(test_mail.TestMailMockups): def setUp(self): super(test_message_compose, self).setUp() @@ -40,11 +33,6 @@ class test_message_compose(common.TransactionCase): self.res_users = self.registry('res.users') self.res_partner = self.registry('res.partner') - # Install mock SMTP gateway - self.build_email_real = self.registry('ir.mail_server').build_email - self.registry('ir.mail_server').build_email = self._mock_build_email - self.registry('ir.mail_server').send_email = self._mock_smtp_gateway - # create a 'pigs' and 'bird' groups that will be used through the various tests self.group_pigs_id = self.mail_group.create(self.cr, self.uid, {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) diff --git a/addons/portal/tests/test_portal.py b/addons/portal/tests/test_portal.py index 5f2875711df..1f709ed3fa9 100644 --- a/addons/portal/tests/test_portal.py +++ b/addons/portal/tests/test_portal.py @@ -20,7 +20,6 @@ ############################################################################## from openerp.addons.mail.tests import test_mail -# from openerp.tests import common from openerp.tools import append_content_to_html From b2c79d266b7a2752f26782ee673e52b647825f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 13 Sep 2012 14:11:10 +0200 Subject: [PATCH 56/82] [FIX] mail_message: fixed record_name, because we could call a name_get on a document we can not see (if we are in the recipints of a document we can not access). name_get is therefore inside a try/except statement, getting a void string as result if AccessDenied is raised. Updated JS to avoid displaying the record name on a void string. bzr revid: tde@openerp.com-20120913121110-bd0syf813h0e39gk --- addons/mail/mail_message.py | 6 +++++- addons/mail/static/src/xml/mail.xml | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 10f02805ab3..234c0262f6a 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -20,6 +20,7 @@ ############################################################################## import logging +import openerp import tools from email.header import decode_header @@ -58,7 +59,10 @@ class mail_message(osv.Model): for message in self.browse(cr, uid, ids, context=context): if not message.model or not message.res_id: continue - result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]) + try: + result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]) + except openerp.exceptions.AccessDenied, e: + pass return result def _get_unread(self, cr, uid, ids, name, arg, context=None): diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 4d3b56cf62f..10aa7c07ec7 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -130,14 +130,14 @@
    - +