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/70] [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/70] [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/70] [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/70] [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/70] 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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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/70] [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 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 22/70] [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 23/70] [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 24/70] [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 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 25/70] [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 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 26/70] [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 27/70] [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 28/70] [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 29/70] [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 30/70] [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 31/70] [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 32/70] [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 33/70] [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 34/70] [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 35/70] [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 36/70] [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 37/70] [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 38/70] [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 39/70] [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 40/70] [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 45/70] [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 46/70] [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 47/70] [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 48/70] [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 49/70] [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 @@
    - +