diff --git a/addons/account/__openerp__.py b/addons/account/__openerp__.py
index cd19c730a3f..e1b9078ec3f 100644
--- a/addons/account/__openerp__.py
+++ b/addons/account/__openerp__.py
@@ -29,25 +29,23 @@ Accounting and Financial Management.
Financial and accounting module that covers:
--------------------------------------------
-General accountings
-Cost / Analytic accounting
-Third party accounting
-Taxes management
-Budgets
-Customer and Supplier Invoices
-Bank statements
-Reconciliation process by partner
+ * General Accounting
+ * Cost/Analytic accounting
+ * Third party accounting
+ * Taxes management
+ * Budgets
+ * Customer and Supplier Invoices
+ * Bank statements
+ * Reconciliation process by partner
Creates a dashboard for accountants that includes:
--------------------------------------------------
-* List of Customer Invoice to Approve
-* Company Analysis
-* Graph of Aged Receivables
-* Graph of Treasury
+ * List of Customer Invoice to Approve
+ * Company Analysis
+ * Graph of Treasury
-The processes like maintaining of general ledger is done through the defined financial Journals (entry move line or
-grouping is maintained through journal) for a particular financial year and for preparation of vouchers there is a
-module named account_voucher.
+The processes like maintaining of general ledger is done through the defined financial Journals (entry move line orgrouping is maintained through journal) for a particular
+financial year and for preparation of vouchers there is a module named account_voucher.
""",
'website': 'http://www.openerp.com',
'images' : ['images/accounts.jpeg','images/bank_statement.jpeg','images/cash_register.jpeg','images/chart_of_accounts.jpeg','images/customer_invoice.jpeg','images/journal_entries.jpeg'],
@@ -99,6 +97,7 @@ module named account_voucher.
'wizard/account_reconcile_partner_process_view.xml',
'wizard/account_automatic_reconcile_view.xml',
'wizard/account_financial_report_view.xml',
+ 'wizard/pos_box.xml',
'project/wizard/project_account_analytic_line_view.xml',
'account_end_fy.xml',
'account_invoice_view.xml',
@@ -145,8 +144,8 @@ module named account_voucher.
'test/account_use_model.yml',
'test/account_validate_account_move.yml',
'test/account_fiscalyear_close.yml',
- 'test/account_bank_statement.yml',
- 'test/account_cash_statement.yml',
+ #'test/account_bank_statement.yml',
+ #'test/account_cash_statement.yml',
'test/test_edi_invoice.yml',
'test/account_report.yml',
'test/account_fiscalyear_close_state.yml', #last test, as it will definitively close the demo fiscalyear
diff --git a/addons/account/account.py b/addons/account/account.py
index 571ea40698d..cbba0237c78 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -29,6 +29,8 @@ import pooler
from osv import fields, osv
import decimal_precision as dp
from tools.translate import _
+from tools.float_utils import float_round
+
_logger = logging.getLogger(__name__)
def check_cycle(self, cr, uid, ids, context=None):
@@ -101,7 +103,7 @@ class account_payment_term_line(osv.osv):
'value': fields.selection([('procent', 'Percent'),
('balance', 'Balance'),
('fixed', 'Fixed Amount')], 'Valuation',
- required=True, help="""Select here the kind of valuation related to this payment term line. Note that you should have your last line with the type 'Balance' to ensure that the whole amount will be threated."""),
+ required=True, help="""Select here the kind of valuation related to this payment term line. Note that you should have your last line with the type 'Balance' to ensure that the whole amount will be treated."""),
'value_amount': fields.float('Amount To Pay', digits_compute=dp.get_precision('Payment Term'), help="For percent enter a ratio between 0-1."),
'days': fields.integer('Number of Days', required=True, help="Number of days to add before computation of the day of month." \
@@ -313,8 +315,8 @@ class account_account(osv.osv):
cr.execute(request, params)
_logger.debug('Status: %s',(cr.statusmessage))
- for res in cr.dictfetchall():
- accounts[res['id']] = res
+ for row in cr.dictfetchall():
+ accounts[row['id']] = row
# consolidate accounts with direct children
children_and_consolidated.reverse()
@@ -474,7 +476,7 @@ class account_account(osv.osv):
'shortcut': fields.char('Shortcut', size=12),
'tax_ids': fields.many2many('account.tax', 'account_account_tax_default_rel',
'account_id', 'tax_id', 'Default Taxes'),
- 'note': fields.text('Note'),
+ 'note': fields.text('Internal Notes'),
'company_currency_id': fields.function(_get_company_currency, type='many2one', relation='res.currency', string='Company Currency'),
'company_id': fields.many2one('res.company', 'Company', required=True),
'active': fields.boolean('Active', select=2, help="If the active field is set to False, it will allow you to hide the account without removing it."),
@@ -714,6 +716,7 @@ class account_journal(osv.osv):
_name = "account.journal"
_description = "Journal"
_columns = {
+ 'with_last_closing_balance' : fields.boolean('Opening With Last Closing Balance'),
'name': fields.char('Journal Name', size=64, required=True),
'code': fields.char('Code', size=5, required=True, help="The code will be displayed on reports."),
'type': fields.selection([('sale', 'Sale'),('sale_refund','Sale Refund'), ('purchase', 'Purchase'), ('purchase_refund','Purchase Refund'), ('cash', 'Cash'), ('bank', 'Bank and Cheques'), ('general', 'General'), ('situation', 'Opening/Closing Situation')], 'Type', size=32, required=True,
@@ -730,16 +733,21 @@ class account_journal(osv.osv):
'centralisation': fields.boolean('Centralised counterpart', help="Check this box to determine that each entry of this journal won't create a new counterpart but will share the same counterpart. This is used in fiscal year closing."),
'update_posted': fields.boolean('Allow Cancelling Entries', help="Check this box if you want to allow the cancellation the entries related to this journal or of the invoice related to this journal"),
'group_invoice_lines': fields.boolean('Group Invoice Lines', help="If this box is checked, the system will try to group the accounting lines when generating them from invoices."),
- 'sequence_id': fields.many2one('ir.sequence', 'Entry Sequence', help="This field contains the informatin related to the numbering of the journal entries of this journal.", required=True),
+ 'sequence_id': fields.many2one('ir.sequence', 'Entry Sequence', help="This field contains the information related to the numbering of the journal entries of this journal.", required=True),
'user_id': fields.many2one('res.users', 'User', help="The user responsible for this journal"),
'groups_id': fields.many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', 'Groups'),
'currency': fields.many2one('res.currency', 'Currency', help='The currency used to enter statement'),
'entry_posted': fields.boolean('Skip \'Draft\' State for Manual Entries', help='Check this box if you don\'t want new journal entries to pass through the \'draft\' state and instead goes directly to the \'posted state\' without any manual validation. \nNote that journal entries that are automatically created by the system are always skipping that state.'),
'company_id': fields.many2one('res.company', 'Company', required=True, select=1, help="Company related to this journal"),
'allow_date':fields.boolean('Check Date in Period', help= 'If set to True then do not accept the entry if the entry date is not into the period dates'),
+
+ 'profit_account_id' : fields.many2one('account.account', 'Profit Account'),
+ 'loss_account_id' : fields.many2one('account.account', 'Loss Account'),
+ 'internal_account_id' : fields.many2one('account.account', 'Internal Transfers Account', select=1),
}
_defaults = {
+ 'with_last_closing_balance' : False,
'user_id': lambda self, cr, uid, context: uid,
'company_id': lambda self, cr, uid, c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
}
@@ -1263,7 +1271,7 @@ class account_move(osv.osv):
'period_id': fields.many2one('account.period', 'Period', required=True, states={'posted':[('readonly',True)]}),
'journal_id': fields.many2one('account.journal', 'Journal', required=True, states={'posted':[('readonly',True)]}),
'state': fields.selection([('draft','Unposted'), ('posted','Posted')], 'Status', required=True, readonly=True,
- help='All manually created new journal entries are usually in the state \'Unposted\', but you can set the option to skip that state on the related journal. In that case, they will be behave as journal entries automatically created by the system on document validation (invoices, bank statements...) and will be created in \'Posted\' state.'),
+ help='All manually created new journal entries are usually in the state \'Unposted\', but you can set the option to skip that state on the related journal. In that case, they will behave as journal entries automatically created by the system on document validation (invoices, bank statements...) and will be created in \'Posted\' state.'),
'line_id': fields.one2many('account.move.line', 'move_id', 'Entries', states={'posted':[('readonly',True)]}),
'to_check': fields.boolean('To Review', help='Check this box if you are unsure of that journal entry and if you want to note it as \'to be reviewed\' by an accounting expert.'),
'partner_id': fields.related('line_id', 'partner_id', type="many2one", relation="res.partner", string="Partner", store=True),
@@ -1782,7 +1790,7 @@ class account_tax_code(osv.osv):
'line_ids': fields.one2many('account.move.line', 'tax_code_id', 'Lines'),
'company_id': fields.many2one('res.company', 'Company', required=True),
'sign': fields.float('Coefficent for parent', required=True, help='You can specify here the coefficient that will be used when consolidating the amount of this case into its parent. For example, set 1/-1 if you want to add/substract it.'),
- 'notprintable':fields.boolean("Not Printable in Invoice", help="Check this box if you don't want any VAT related to this Tax Code to appear on invoices"),
+ 'notprintable':fields.boolean("Not Printable in Invoice", help="Check this box if you don't want any tax related to this tax code to appear on invoices"),
'sequence': fields.integer('Sequence', help="Determine the display order in the report 'Accounting \ Reporting \ Generic Reporting \ Taxes \ Taxes Report'"),
}
@@ -1874,17 +1882,17 @@ class account_tax(osv.osv):
'python_applicable':fields.text('Python Code'),
#
- # Fields used for the VAT declaration
+ # Fields used for the Tax declaration
#
- 'base_code_id': fields.many2one('account.tax.code', 'Account Base Code', help="Use this code for the VAT declaration."),
- 'tax_code_id': fields.many2one('account.tax.code', 'Account Tax Code', help="Use this code for the VAT declaration."),
+ 'base_code_id': fields.many2one('account.tax.code', 'Account Base Code', help="Use this code for the tax declaration."),
+ 'tax_code_id': fields.many2one('account.tax.code', 'Account Tax Code', help="Use this code for the tax declaration."),
'base_sign': fields.float('Base Code Sign', help="Usually 1 or -1."),
'tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1."),
# Same fields for refund invoices
- 'ref_base_code_id': fields.many2one('account.tax.code', 'Refund Base Code', help="Use this code for the VAT declaration."),
- 'ref_tax_code_id': fields.many2one('account.tax.code', 'Refund Tax Code', help="Use this code for the VAT declaration."),
+ 'ref_base_code_id': fields.many2one('account.tax.code', 'Refund Base Code', help="Use this code for the tax declaration."),
+ 'ref_tax_code_id': fields.many2one('account.tax.code', 'Refund Tax Code', help="Use this code for the tax declaration."),
'ref_base_sign': fields.float('Base Code Sign', help="Usually 1 or -1."),
'ref_tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1."),
'include_base_amount': fields.boolean('Included in base amount', help="Indicates if the amount of tax must be included in the base amount for the computation of the next taxes"),
@@ -2084,7 +2092,7 @@ class account_tax(osv.osv):
tax_compute_precision = precision
if taxes and taxes[0].company_id.tax_calculation_rounding_method == 'round_globally':
tax_compute_precision += 5
- totalin = totalex = round(price_unit * quantity, precision)
+ totalin = totalex = float_round(price_unit * quantity, precision)
tin = []
tex = []
for tax in taxes:
@@ -2211,7 +2219,7 @@ class account_tax(osv.osv):
def compute_inv(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None, precision=None):
"""
Compute tax values for given PRICE_UNIT, QUANTITY and a buyer/seller ADDRESS_ID.
- Price Unit is a VAT included price
+ Price Unit is a Tax included price
RETURN:
[ tax ]
@@ -2667,7 +2675,7 @@ class account_tax_code_template(osv.osv):
'parent_id': fields.many2one('account.tax.code.template', 'Parent Code', select=True),
'child_ids': fields.one2many('account.tax.code.template', 'parent_id', 'Child Codes'),
'sign': fields.float('Sign For Parent', required=True),
- 'notprintable':fields.boolean("Not Printable in Invoice", help="Check this box if you don't want any VAT related to this Tax Code to appear on invoices"),
+ 'notprintable':fields.boolean("Not Printable in Invoice", help="Check this box if you don't want any tax related to this tax Code to appear on invoices."),
}
_defaults = {
@@ -2782,17 +2790,17 @@ class account_tax_template(osv.osv):
'python_applicable':fields.text('Python Code'),
#
- # Fields used for the VAT declaration
+ # Fields used for the Tax declaration
#
- 'base_code_id': fields.many2one('account.tax.code.template', 'Base Code', help="Use this code for the VAT declaration."),
- 'tax_code_id': fields.many2one('account.tax.code.template', 'Tax Code', help="Use this code for the VAT declaration."),
+ 'base_code_id': fields.many2one('account.tax.code.template', 'Base Code', help="Use this code for the tax declaration."),
+ 'tax_code_id': fields.many2one('account.tax.code.template', 'Tax Code', help="Use this code for the tax declaration."),
'base_sign': fields.float('Base Code Sign', help="Usually 1 or -1."),
'tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1."),
# Same fields for refund invoices
- 'ref_base_code_id': fields.many2one('account.tax.code.template', 'Refund Base Code', help="Use this code for the VAT declaration."),
- 'ref_tax_code_id': fields.many2one('account.tax.code.template', 'Refund Tax Code', help="Use this code for the VAT declaration."),
+ 'ref_base_code_id': fields.many2one('account.tax.code.template', 'Refund Base Code', help="Use this code for the tax declaration."),
+ 'ref_tax_code_id': fields.many2one('account.tax.code.template', 'Refund Tax Code', help="Use this code for the tax declaration."),
'ref_base_sign': fields.float('Base Code Sign', help="Usually 1 or -1."),
'ref_tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1."),
'include_base_amount': fields.boolean('Include in Base Amount', help="Set if the amount of tax must be included in the base amount before computing the next taxes."),
@@ -3345,15 +3353,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
# Create Bank journals
self._create_bank_journals_from_o2m(cr, uid, obj_wizard, company_id, acc_template_ref, context=context)
- action = {
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'board.board',
- 'view_id': self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'board_account_form')[1],
- 'menu_id': self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'menu_finance')[1]
- }
- return action
+ return {}
def _prepare_bank_journal(self, cr, uid, line, current_num, default_account_id, company_id, context=None):
'''
diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py
index 1f47a6be9bd..11bcf565260 100644
--- a/addons/account/account_bank_statement.py
+++ b/addons/account/account_bank_statement.py
@@ -26,24 +26,18 @@ from tools.translate import _
import decimal_precision as dp
class account_bank_statement(osv.osv):
-
def create(self, cr, uid, vals, context=None):
- seq = 0
if 'line_ids' in vals:
- new_line_ids = []
- for line in vals['line_ids']:
- seq += 1
- line[2]['sequence'] = seq
+ for idx, line in enumerate(vals['line_ids']):
+ line[2]['sequence'] = idx + 1
return super(account_bank_statement, self).create(cr, uid, vals, context=context)
def write(self, cr, uid, ids, vals, context=None):
res = super(account_bank_statement, self).write(cr, uid, ids, vals, context=context)
account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
for statement in self.browse(cr, uid, ids, context):
- seq = 0
- for line in statement.line_ids:
- seq += 1
- account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': seq}, context=context)
+ for idx, line in enumerate(statement.line_ids):
+ account_bank_statement_line_obj.write(cr, uid, [line.id], {'sequence': idx + 1}, context=context)
return res
def _default_journal_id(self, cr, uid, context=None):
@@ -51,45 +45,19 @@ class account_bank_statement(osv.osv):
context = {}
journal_pool = self.pool.get('account.journal')
journal_type = context.get('journal_type', False)
- journal_id = False
company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement',context=context)
if journal_type:
ids = journal_pool.search(cr, uid, [('type', '=', journal_type),('company_id','=',company_id)])
if ids:
- journal_id = ids[0]
- return journal_id
+ return ids[0]
+ return False
def _end_balance(self, cursor, user, ids, name, attr, context=None):
- res_currency_obj = self.pool.get('res.currency')
- res_users_obj = self.pool.get('res.users')
res = {}
-
- company_currency_id = res_users_obj.browse(cursor, user, user,
- context=context).company_id.currency_id.id
-
- statements = self.browse(cursor, user, ids, context=context)
- for statement in statements:
+ for statement in self.browse(cursor, user, ids, context=context):
res[statement.id] = statement.balance_start
- currency_id = statement.currency.id
- for line in statement.move_line_ids:
- if line.debit > 0:
- if line.account_id.id == \
- statement.journal_id.default_debit_account_id.id:
- res[statement.id] += res_currency_obj.compute(cursor,
- user, company_currency_id, currency_id,
- line.debit, context=context)
- else:
- if line.account_id.id == \
- statement.journal_id.default_credit_account_id.id:
- res[statement.id] -= res_currency_obj.compute(cursor,
- user, company_currency_id, currency_id,
- line.credit, context=context)
-
- if statement.state in ('draft', 'open'):
- for line in statement.line_ids:
- res[statement.id] += line.amount
- for r in res:
- res[r] = round(res[r], 2)
+ for line in statement.line_ids:
+ res[statement.id] += line.amount
return res
def _get_period(self, cr, uid, context=None):
@@ -129,7 +97,7 @@ class account_bank_statement(osv.osv):
_description = "Bank Statement"
_inherit = ['mail.thread']
_columns = {
- 'name': fields.char('Name', size=64, required=True, states={'draft': [('readonly', False)]}, readonly=True, help='if you give the Name other then /, its created Accounting Entries Move will be with same name as statement name. This allows the statement entries to have the same references than the statement itself'), # readonly for account_cash_statement
+ 'name': fields.char('Reference', size=64, required=True, states={'draft': [('readonly', False)]}, readonly=True, help='if you give the Name other then /, its created Accounting Entries Move will be with same name as statement name. This allows the statement entries to have the same references than the statement itself'), # readonly for account_cash_statement
'date': fields.date('Date', required=True, states={'confirm': [('readonly', True)]}, select=True),
'journal_id': fields.many2one('account.journal', 'Journal', required=True,
readonly=True, states={'draft':[('readonly',False)]}),
@@ -141,7 +109,7 @@ class account_bank_statement(osv.osv):
states={'confirm': [('readonly', True)]}),
'balance_end': fields.function(_end_balance,
store = {
- 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids'], 10),
+ 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids','balance_start'], 10),
'account.bank.statement.line': (_get_statement, ['amount'], 10),
},
string="Computed Balance", help='Balance as calculated based on Starting Balance and transaction lines'),
@@ -205,7 +173,158 @@ class account_bank_statement(osv.osv):
def button_dummy(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {}, context=context)
+ def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
+ """Prepare the dict of values to create the move from a
+ statement line. This method may be overridden to implement custom
+ move generation (making sure to call super() to establish
+ a clean extension chain).
+
+ :param browse_record st_line: account.bank.statement.line record to
+ create the move from.
+ :param char st_line_number: will be used as the name of the generated account move
+ :return: dict of value to create() the account.move
+ """
+ return {
+ 'journal_id': st_line.statement_id.journal_id.id,
+ 'period_id': st_line.statement_id.period_id.id,
+ 'date': st_line.date,
+ 'name': st_line_number,
+ 'ref': st_line.ref,
+ }
+
+ def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id,
+ context=None):
+ """Compute the args to build the dict of values to create the bank move line from a
+ statement line by calling the _prepare_move_line_vals. This method may be
+ overridden to implement custom move generation (making sure to call super() to
+ establish a clean extension chain).
+
+ :param browse_record st_line: account.bank.statement.line record to
+ create the move from.
+ :param int/long move_id: ID of the account.move to link the move line
+ :param float amount: amount of the move line
+ :param int/long company_currency_id: ID of currency of the concerned company
+ :return: dict of value to create() the bank account.move.line
+ """
+ anl_id = st_line.analytic_account_id and st_line.analytic_account_id.id or False
+ debit = ((amount<0) and -amount) or 0.0
+ credit = ((amount>0) and amount) or 0.0
+ cur_id = False
+ amt_cur = False
+ if st_line.statement_id.currency.id <> company_currency_id:
+ cur_id = st_line.statement_id.currency.id
+ if st_line.account_id and st_line.account_id.currency_id and st_line.account_id.currency_id.id <> company_currency_id:
+ cur_id = st_line.account_id.currency_id.id
+ if cur_id:
+ res_currency_obj = self.pool.get('res.currency')
+ amt_cur = -res_currency_obj.compute(cr, uid, company_currency_id, cur_id, amount, context=context)
+
+ res = self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit,
+ amount_currency=amt_cur, currency_id=cur_id, analytic_id=anl_id, context=context)
+ return res
+
+ def _get_counter_part_account(sefl, cr, uid, st_line, context=None):
+ """Retrieve the account to use in the counterpart move.
+ This method may be overridden to implement custom move generation (making sure to
+ call super() to establish a clean extension chain).
+
+ :param browse_record st_line: account.bank.statement.line record to
+ create the move from.
+ :return: int/long of the account.account to use as counterpart
+ """
+ if st_line.amount >= 0:
+ return st_line.statement_id.journal_id.default_credit_account_id.id
+ return st_line.statement_id.journal_id.default_debit_account_id.id
+
+ def _get_counter_part_partner(sefl, cr, uid, st_line, context=None):
+ """Retrieve the partner to use in the counterpart move.
+ This method may be overridden to implement custom move generation (making sure to
+ call super() to establish a clean extension chain).
+
+ :param browse_record st_line: account.bank.statement.line record to
+ create the move from.
+ :return: int/long of the res.partner to use as counterpart
+ """
+ return st_line.partner_id and st_line.partner_id.id or False
+
+ def _prepare_counterpart_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id,
+ context=None):
+ """Compute the args to build the dict of values to create the counter part move line from a
+ statement line by calling the _prepare_move_line_vals. This method may be
+ overridden to implement custom move generation (making sure to call super() to
+ establish a clean extension chain).
+
+ :param browse_record st_line: account.bank.statement.line record to
+ create the move from.
+ :param int/long move_id: ID of the account.move to link the move line
+ :param float amount: amount of the move line
+ :param int/long account_id: ID of account to use as counter part
+ :param int/long company_currency_id: ID of currency of the concerned company
+ :return: dict of value to create() the bank account.move.line
+ """
+ account_id = self._get_counter_part_account(cr, uid, st_line, context=context)
+ partner_id = self._get_counter_part_partner(cr, uid, st_line, context=context)
+ debit = ((amount > 0) and amount) or 0.0
+ credit = ((amount < 0) and -amount) or 0.0
+ cur_id = False
+ amt_cur = False
+ if st_line.statement_id.currency.id <> company_currency_id:
+ amt_cur = st_line.amount
+ cur_id = st_line.statement_id.currency.id
+ return self._prepare_move_line_vals(cr, uid, st_line, move_id, debit, credit,
+ amount_currency = amt_cur, currency_id = cur_id, account_id = account_id,
+ partner_id = partner_id, context=context)
+
+ def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id = False,
+ amount_currency= False, account_id = False, analytic_id = False,
+ partner_id = False, context=None):
+ """Prepare the dict of values to create the move line from a
+ statement line. All non-mandatory args will replace the default computed one.
+ This method may be overridden to implement custom move generation (making sure to
+ call super() to establish a clean extension chain).
+
+ :param browse_record st_line: account.bank.statement.line record to
+ create the move from.
+ :param int/long move_id: ID of the account.move to link the move line
+ :param float debit: debit amount of the move line
+ :param float credit: credit amount of the move line
+ :param int/long currency_id: ID of currency of the move line to create
+ :param float amount_currency: amount of the debit/credit expressed in the currency_id
+ :param int/long account_id: ID of the account to use in the move line if different
+ from the statement line account ID
+ :param int/long analytic_id: ID of analytic account to put on the move line
+ :param int/long partner_id: ID of the partner to put on the move line
+ :return: dict of value to create() the account.move.line
+ """
+ acc_id = account_id or st_line.account_id.id
+ cur_id = currency_id or st_line.statement_id.currency.id
+ par_id = partner_id or (((st_line.partner_id) and st_line.partner_id.id) or False)
+ return {
+ 'name': st_line.name,
+ 'date': st_line.date,
+ 'ref': st_line.ref,
+ 'move_id': move_id,
+ 'partner_id': partner_id,
+ 'account_id': acc_id,
+ 'credit': credit,
+ 'debit': debit,
+ 'statement_id': st_line.statement_id.id,
+ 'journal_id': st_line.statement_id.journal_id.id,
+ 'period_id': st_line.statement_id.period_id.id,
+ 'currency_id': cur_id,
+ 'amount_currency': amount_currency,
+ 'analytic_account_id': analytic_id,
+ }
+
def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id, st_line_number, context=None):
+ """Create the account move from the statement line.
+
+ :param int/long st_line_id: ID of the account.bank.statement.line to create the move from.
+ :param int/long company_currency_id: ID of the res.currency of the company
+ :param char st_line_number: will be used as the name of the generated account move
+ :return: ID of the account.move created
+ """
+
if context is None:
context = {}
res_currency_obj = self.pool.get('res.currency')
@@ -217,82 +336,28 @@ class account_bank_statement(osv.osv):
context.update({'date': st_line.date})
- move_id = account_move_obj.create(cr, uid, {
- 'journal_id': st.journal_id.id,
- 'period_id': st.period_id.id,
- 'date': st_line.date,
- 'name': st_line_number,
- 'ref': st_line.ref,
- }, context=context)
+ move_vals = self._prepare_move(cr, uid, st_line, st_line_number, context=context)
+ move_id = account_move_obj.create(cr, uid, move_vals, context=context)
account_bank_statement_line_obj.write(cr, uid, [st_line.id], {
'move_ids': [(4, move_id, False)]
})
-
torec = []
- if st_line.amount >= 0:
- account_id = st.journal_id.default_credit_account_id.id
- else:
- account_id = st.journal_id.default_debit_account_id.id
-
acc_cur = ((st_line.amount<=0) and st.journal_id.default_debit_account_id) or st_line.account_id
+
context.update({
'res.currency.compute.account': acc_cur,
})
amount = res_currency_obj.compute(cr, uid, st.currency.id,
company_currency_id, st_line.amount, context=context)
- val = {
- 'name': st_line.name,
- 'date': st_line.date,
- 'ref': st_line.ref,
- 'move_id': move_id,
- 'partner_id': ((st_line.partner_id) and st_line.partner_id.id) or False,
- 'account_id': (st_line.account_id) and st_line.account_id.id,
- 'credit': ((amount>0) and amount) or 0.0,
- 'debit': ((amount<0) and -amount) or 0.0,
- 'statement_id': st.id,
- 'journal_id': st.journal_id.id,
- 'period_id': st.period_id.id,
- 'currency_id': st.currency.id,
- 'analytic_account_id': st_line.analytic_account_id and st_line.analytic_account_id.id or False
- }
-
- if st.currency.id <> company_currency_id:
- amount_cur = res_currency_obj.compute(cr, uid, company_currency_id,
- st.currency.id, amount, context=context)
- val['amount_currency'] = -amount_cur
-
- if st_line.account_id and st_line.account_id.currency_id and st_line.account_id.currency_id.id <> company_currency_id:
- val['currency_id'] = st_line.account_id.currency_id.id
- amount_cur = res_currency_obj.compute(cr, uid, company_currency_id,
- st_line.account_id.currency_id.id, amount, context=context)
- val['amount_currency'] = -amount_cur
-
- move_line_id = account_move_line_obj.create(cr, uid, val, context=context)
+ bank_move_vals = self._prepare_bank_move_line(cr, uid, st_line, move_id, amount,
+ company_currency_id, context=context)
+ move_line_id = account_move_line_obj.create(cr, uid, bank_move_vals, context=context)
torec.append(move_line_id)
- # Fill the secondary amount/currency
- # if currency is not the same than the company
- amount_currency = False
- currency_id = False
- if st.currency.id <> company_currency_id:
- amount_currency = st_line.amount
- currency_id = st.currency.id
- account_move_line_obj.create(cr, uid, {
- 'name': st_line.name,
- 'date': st_line.date,
- 'ref': st_line.ref,
- 'move_id': move_id,
- 'partner_id': ((st_line.partner_id) and st_line.partner_id.id) or False,
- 'account_id': account_id,
- 'credit': ((amount < 0) and -amount) or 0.0,
- 'debit': ((amount > 0) and amount) or 0.0,
- 'statement_id': st.id,
- 'journal_id': st.journal_id.id,
- 'period_id': st.period_id.id,
- 'amount_currency': amount_currency,
- 'currency_id': currency_id,
- }, context=context)
+ counterpart_move_vals = self._prepare_counterpart_move_line(cr, uid, st_line, move_id,
+ amount, company_currency_id, context=context)
+ account_move_line_obj.create(cr, uid, counterpart_move_vals, context=context)
for line in account_move_line_obj.browse(cr, uid, [x.id for x in
account_move_obj.browse(cr, uid, move_id,
@@ -311,7 +376,7 @@ class account_bank_statement(osv.osv):
def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
st = self.browse(cr, uid, st_id, context=context)
- if not ((abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001) or (abs((st.balance_end or 0.0) - st.balance_end_cash) < 0.0001)):
+ if not ((abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001) or (abs((st.balance_end or 0.0) - st.balance_end_real) < 0.0001)):
raise osv.except_osv(_('Error !'),
_('The statement balance is incorrect !\nThe expected balance (%.2f) is different than the computed one. (%.2f)') % (st.balance_end_real, st.balance_end))
return True
@@ -380,14 +445,18 @@ class account_bank_statement(osv.osv):
account_move_obj.unlink(cr, uid, ids, context)
done.append(st.id)
return self.write(cr, uid, done, {'state':'draft'}, context=context)
-
- def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
+
+ def _compute_balance_end_real(self, cr, uid, journal_id, context=None):
cr.execute('SELECT balance_end_real \
FROM account_bank_statement \
WHERE journal_id = %s AND NOT state = %s \
ORDER BY date DESC,id DESC LIMIT 1', (journal_id, 'draft'))
res = cr.fetchone()
- balance_start = res and res[0] or 0.0
+ return res and res[0] or 0.0
+
+ def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
+ balance_start = self._compute_balance_end_real(cr, uid, journal_id, context=context)
+
journal_data = self.pool.get('account.journal').read(cr, uid, journal_id, ['default_debit_account_id', 'company_id'], context=context)
account_id = journal_data['default_debit_account_id']
company_id = journal_data['company_id']
diff --git a/addons/account/account_cash_statement.py b/addons/account/account_cash_statement.py
index f19c1e1c639..927ce94db31 100644
--- a/addons/account/account_cash_statement.py
+++ b/addons/account/account_cash_statement.py
@@ -43,24 +43,27 @@ class account_cashbox_line(osv.osv):
"""
res = {}
for obj in self.browse(cr, uid, ids, context=context):
- res[obj.id] = obj.pieces * obj.number
+ res[obj.id] = {
+ 'subtotal_opening' : obj.pieces * obj.number_opening,
+ 'subtotal_closing' : obj.pieces * obj.number_closing,
+ }
return res
- def on_change_sub(self, cr, uid, ids, pieces, number, *a):
+ def on_change_sub_opening(self, cr, uid, ids, pieces, number, *a):
+ """ Compute the subtotal for the opening """
+ return {'value' : {'subtotal_opening' : (pieces * number) or 0.0 }}
- """ Calculates Sub total on change of number
- @param pieces: Names of fields.
- @param number:
- """
- sub = pieces * number
- return {'value': {'subtotal': sub or 0.0}}
+ def on_change_sub_closing(self, cr, uid, ids, pieces, number, *a):
+ """ Compute the subtotal for the closing """
+ return {'value' : {'subtotal_closing' : (pieces * number) or 0.0 }}
_columns = {
- 'pieces': fields.float('Values', digits_compute=dp.get_precision('Account')),
- 'number': fields.integer('Number'),
- 'subtotal': fields.function(_sub_total, string='Sub Total', type='float', digits_compute=dp.get_precision('Account')),
- 'starting_id': fields.many2one('account.bank.statement', ondelete='cascade'),
- 'ending_id': fields.many2one('account.bank.statement', ondelete='cascade'),
+ 'pieces': fields.float('Unit of Currency', digits_compute=dp.get_precision('Account')),
+ 'number_opening' : fields.integer('Number of Units', help='Opening Unit Numbers'),
+ 'number_closing' : fields.integer('Number of Units', help='Closing Unit Numbers'),
+ 'subtotal_opening': fields.function(_sub_total, string='Opening Subtotal', type='float', digits_compute=dp.get_precision('Account'), multi='subtotal'),
+ 'subtotal_closing': fields.function(_sub_total, string='Closing Subtotal', type='float', digits_compute=dp.get_precision('Account'), multi='subtotal'),
+ 'bank_statement_id' : fields.many2one('account.bank.statement', ondelete='cascade'),
}
account_cashbox_line()
@@ -69,39 +72,24 @@ class account_cash_statement(osv.osv):
_inherit = 'account.bank.statement'
- def _get_starting_balance(self, cr, uid, ids, context=None):
-
- """ Find starting balance
- @param name: Names of fields.
- @param arg: User defined arguments
- @return: Dictionary of values.
+ def _update_balances(self, cr, uid, ids, context=None):
+ """
+ Set starting and ending balances according to pieces count
"""
res = {}
for statement in self.browse(cr, uid, ids, context=context):
- amount_total = 0.0
-
- if statement.journal_id.type not in('cash'):
+ if statement.journal_id.type not in ('cash',):
continue
-
- for line in statement.starting_details_ids:
- amount_total+= line.pieces * line.number
- res[statement.id] = {
- 'balance_start': amount_total
+ start = end = 0
+ for line in statement.details_ids:
+ start += line.subtotal_opening
+ end += line.subtotal_closing
+ data = {
+ 'balance_start': start,
+ 'balance_end_real': end,
}
- return res
-
- def _balance_end_cash(self, cr, uid, ids, name, arg, context=None):
- """ Find ending balance "
- @param name: Names of fields.
- @param arg: User defined arguments
- @return: Dictionary of values.
- """
- res = {}
- for statement in self.browse(cr, uid, ids, context=context):
- amount_total = 0.0
- for line in statement.ending_details_ids:
- amount_total += line.pieces * line.number
- res[statement.id] = amount_total
+ res[statement.id] = data
+ super(account_cash_statement, self).write(cr, uid, [statement.id], data, context=context)
return res
def _get_sum_entry_encoding(self, cr, uid, ids, name, arg, context=None):
@@ -111,13 +99,10 @@ class account_cash_statement(osv.osv):
@param arg: User defined arguments
@return: Dictionary of values.
"""
- res2 = {}
+ res = {}
for statement in self.browse(cr, uid, ids, context=context):
- encoding_total=0.0
- for line in statement.line_ids:
- encoding_total += line.amount
- res2[statement.id] = encoding_total
- return res2
+ res[statement.id] = sum((line.amount for line in statement.line_ids), 0.0)
+ return res
def _get_company(self, cr, uid, context=None):
user_pool = self.pool.get('res.users')
@@ -128,96 +113,98 @@ class account_cash_statement(osv.osv):
company_id = company_pool.search(cr, uid, [])
return company_id and company_id[0] or False
- def _get_cash_open_box_lines(self, cr, uid, context=None):
- res = []
- curr = [1, 2, 5, 10, 20, 50, 100, 500]
- for rs in curr:
- dct = {
- 'pieces': rs,
- 'number': 0
- }
- res.append(dct)
- journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash')], context=context)
- if journal_ids:
- results = self.search(cr, uid, [('journal_id', 'in', journal_ids),('state', '=', 'confirm')], context=context)
- if results:
- cash_st = self.browse(cr, uid, results, context=context)[0]
- for cash_line in cash_st.ending_details_ids:
- for r in res:
- if cash_line.pieces == r['pieces']:
- r['number'] = cash_line.number
- return res
-
- def _get_default_cash_close_box_lines(self, cr, uid, context=None):
- res = []
- curr = [1, 2, 5, 10, 20, 50, 100, 500]
- for rs in curr:
- dct = {
- 'pieces': rs,
- 'number': 0
- }
- res.append(dct)
- return res
-
- def _get_cash_close_box_lines(self, cr, uid, context=None):
- res = []
- curr = [1, 2, 5, 10, 20, 50, 100, 500]
- for rs in curr:
- dct = {
- 'pieces': rs,
- 'number': 0
- }
- res.append((0, 0, dct))
- return res
-
- def _get_cash_open_close_box_lines(self, cr, uid, context=None):
- res = {}
- start_l = []
- end_l = []
- starting_details = self._get_cash_open_box_lines(cr, uid, context=context)
- ending_details = self._get_default_cash_close_box_lines(cr, uid, context)
- for start in starting_details:
- start_l.append((0, 0, start))
- for end in ending_details:
- end_l.append((0, 0, end))
- res['start'] = start_l
- res['end'] = end_l
- return res
-
- def _get_statement(self, cr, uid, ids, context=None):
+ def _get_statement_from_line(self, cr, uid, ids, context=None):
result = {}
for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
result[line.statement_id.id] = True
return result.keys()
+ def _compute_difference(self, cr, uid, ids, fieldnames, args, context=None):
+ result = dict.fromkeys(ids, 0.0)
+
+ for obj in self.browse(cr, uid, ids, context=context):
+ result[obj.id] = obj.balance_end_real - obj.balance_end
+
+ return result
+
+ def _compute_last_closing_balance(self, cr, uid, ids, fieldnames, args, context=None):
+ result = dict.fromkeys(ids, 0.0)
+
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.state == 'draft':
+ statement_ids = self.search(cr, uid,
+ [('journal_id', '=', obj.journal_id.id),('state', '=', 'confirm')],
+ order='create_date desc',
+ limit=1,
+ context=context
+ )
+
+ if not statement_ids:
+ continue
+ else:
+ st = self.browse(cr, uid, statement_ids[0], context=context)
+ result[obj.id] = st.balance_end_real
+
+ return result
+
+ def onchange_journal_id(self, cr, uid, ids, journal_id, context=None):
+ result = super(account_cash_statement, self).onchange_journal_id(cr, uid, ids, journal_id)
+
+ if not journal_id:
+ return result
+
+ statement_ids = self.search(cr, uid,
+ [('journal_id', '=', journal_id),('state', '=', 'confirm')],
+ order='create_date desc',
+ limit=1,
+ context=context
+ )
+
+ if not statement_ids:
+ return result
+
+ st = self.browse(cr, uid, statement_ids[0], context=context)
+ result.setdefault('value', {}).update({'last_closing_balance' : st.balance_end_real})
+
+ return result
+
_columns = {
- 'total_entry_encoding': fields.function(_get_sum_entry_encoding, string="Cash Transaction", help="Total cash transactions",
+ 'total_entry_encoding': fields.function(_get_sum_entry_encoding, string="Total Cash Transactions",
store = {
- 'account.bank.statement': (lambda self, cr, uid, ids, c={}: ids, ['line_ids','move_line_ids'], 10),
- 'account.bank.statement.line': (_get_statement, ['amount'], 10),
+ 'account.bank.statement': (lambda self, cr, uid, ids, context=None: ids, ['line_ids','move_line_ids'], 10),
+ 'account.bank.statement.line': (_get_statement_from_line, ['amount'], 10),
}),
'closing_date': fields.datetime("Closed On"),
- 'balance_end_cash': fields.function(_balance_end_cash, store=True, string='Closing Balance', help="Closing balance based on cashBox"),
- 'starting_details_ids': fields.one2many('account.cashbox.line', 'starting_id', string='Opening Cashbox'),
- 'ending_details_ids': fields.one2many('account.cashbox.line', 'ending_id', string='Closing Cashbox'),
+ 'details_ids' : fields.one2many('account.cashbox.line', 'bank_statement_id', string='CashBox Lines'),
+ 'opening_details_ids' : fields.one2many('account.cashbox.line', 'bank_statement_id', string='Opening Cashbox Lines'),
+ 'closing_details_ids' : fields.one2many('account.cashbox.line', 'bank_statement_id', string='Closing Cashbox Lines'),
'user_id': fields.many2one('res.users', 'Responsible', required=False),
+ 'difference' : fields.function(_compute_difference, method=True, string="Difference", type="float"),
+ 'last_closing_balance' : fields.function(_compute_last_closing_balance, method=True, string='Last Closing Balance', type='float'),
}
_defaults = {
'state': 'draft',
- 'date': lambda self,cr,uid,context={}: context.get('date', time.strftime("%Y-%m-%d %H:%M:%S")),
+ 'date': lambda self, cr, uid, context={}: context.get('date', time.strftime("%Y-%m-%d %H:%M:%S")),
'user_id': lambda self, cr, uid, context=None: uid,
- 'starting_details_ids': _get_cash_open_box_lines,
- 'ending_details_ids': _get_default_cash_close_box_lines
- }
+ }
def create(self, cr, uid, vals, context=None):
- if self.pool.get('account.journal').browse(cr, uid, vals['journal_id'], context=context).type == 'cash':
- amount_total = 0.0
- for line in vals.get('starting_details_ids',[]):
- if line and len(line)==3 and line[2]:
- amount_total+= line[2]['pieces'] * line[2]['number']
- vals.update(balance_start= amount_total)
- return super(account_cash_statement, self).create(cr, uid, vals, context=context)
+ journal = False
+ if vals.get('journal_id'):
+ journal = self.pool.get('account.journal').browse(cr, uid, vals['journal_id'], context=context)
+ if journal and (journal.type == 'cash') and not vals.get('details_ids'):
+ vals['details_ids'] = []
+ for value in journal.cashbox_line_ids:
+ nested_values = {
+ 'number_closing' : 0,
+ 'number_opening' : 0,
+ 'pieces' : value.pieces
+ }
+ vals['details_ids'].append([0, False, nested_values])
+
+ res_id = super(account_cash_statement, self).create(cr, uid, vals, context=context)
+ self._update_balances(cr, uid, [res_id], context)
+ return res_id
def write(self, cr, uid, ids, vals, context=None):
"""
@@ -233,34 +220,9 @@ class account_cash_statement(osv.osv):
@return: True on success, False otherwise
"""
- super(account_cash_statement, self).write(cr, uid, ids, vals, context=context)
- res = self._get_starting_balance(cr, uid, ids)
- for rs in res:
- super(account_cash_statement, self).write(cr, uid, [rs], res.get(rs))
- return True
-
- def onchange_journal_id(self, cr, uid, statement_id, journal_id, context=None):
- """ Changes balance start and starting details if journal_id changes"
- @param statement_id: Changed statement_id
- @param journal_id: Changed journal_id
- @return: Dictionary of changed values
- """
- res = {}
- balance_start = 0.0
- if not journal_id:
- res.update({
- 'balance_start': balance_start
- })
- return res
- return super(account_cash_statement, self).onchange_journal_id(cr, uid, statement_id, journal_id, context=context)
-
- def _equal_balance(self, cr, uid, cash_id, context=None):
- statement = self.browse(cr, uid, cash_id, context=context)
- self.write(cr, uid, [cash_id], {'balance_end_real': statement.balance_end})
- statement.balance_end_real = statement.balance_end
- if statement.balance_end != statement.balance_end_cash:
- return False
- return True
+ res = super(account_cash_statement, self).write(cr, uid, ids, vals, context=context)
+ self._update_balances(cr, uid, ids, context)
+ return res
def _user_allow(self, cr, uid, statement_id, context=None):
return True
@@ -276,7 +238,7 @@ class account_cash_statement(osv.osv):
for statement in statement_pool.browse(cr, uid, ids, context=context):
vals = {}
if not self._user_allow(cr, uid, statement.id, context=context):
- raise osv.except_osv(_('Error !'), (_('User %s does not have rights to access %s journal !') % (statement.user_id.name, statement.journal_id.name)))
+ raise osv.except_osv(_('Error !'), (_('You do not have rights to open this %s journal !') % (statement.journal_id.name, )))
if statement.name and statement.name == '/':
c = {'fiscalyear_id': statement.period_id.fiscalyear_id.id}
@@ -294,13 +256,6 @@ class account_cash_statement(osv.osv):
self.write(cr, uid, [statement.id], vals, context=context)
return True
- def balance_check(self, cr, uid, cash_id, journal_type='bank', context=None):
- if journal_type == 'bank':
- return super(account_cash_statement, self).balance_check(cr, uid, cash_id, journal_type, context)
- if not self._equal_balance(cr, uid, cash_id, context):
- raise osv.except_osv(_('Error !'), _('The closing balance should be the same than the computed balance!'))
- return True
-
def statement_close(self, cr, uid, ids, journal_type='bank', context=None):
if journal_type == 'bank':
return super(account_cash_statement, self).statement_close(cr, uid, ids, journal_type, context)
@@ -317,16 +272,67 @@ class account_cash_statement(osv.osv):
def button_confirm_cash(self, cr, uid, ids, context=None):
super(account_cash_statement, self).button_confirm_bank(cr, uid, ids, context=context)
- return self.write(cr, uid, ids, {'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context)
+ absl_proxy = self.pool.get('account.bank.statement.line')
- def button_cancel(self, cr, uid, ids, context=None):
- cash_box_line_pool = self.pool.get('account.cashbox.line')
- super(account_cash_statement, self).button_cancel(cr, uid, ids, context=context)
- for st in self.browse(cr, uid, ids, context):
- for end in st.ending_details_ids:
- cash_box_line_pool.write(cr, uid, [end.id], {'number': 0})
- return True
+ TABLES = (('Profit', 'profit_account_id'), ('Loss', 'loss_account_id'),)
+
+ for obj in self.browse(cr, uid, ids, context=context):
+ if obj.difference == 0.0:
+ continue
+
+ for item_label, item_account in TALBES:
+ if getattr(obj.journal_id, item_account):
+ raise osv.except_osv(_('Error !'),
+ _('There is no %s Account on the Journal %s') % (item_label, obj.journal_id.name,))
+
+ is_profit = obj.difference < 0.0
+
+ account = getattr(obj.journal_id, TABLES[is_profit][1])
+
+ values = {
+ 'statement_id' : obj.id,
+ 'journal_id' : obj.journal_id.id,
+ 'account_id' : account.id,
+ 'amount' : obj.difference,
+ 'name' : 'Exceptional %s' % TABLES[is_profit][0],
+ }
+
+ absl_proxy.create(cr, uid, values, context=context)
+
+ return self.write(cr, uid, ids, {'closing_date': time.strftime("%Y-%m-%d %H:%M:%S")}, context=context)
account_cash_statement()
+class account_journal(osv.osv):
+ _inherit = 'account.journal'
+
+ def _default_cashbox_line_ids(self, cr, uid, context=None):
+ # Return a list of coins in Euros.
+ result = [
+ dict(pieces=value) for value in [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500]
+ ]
+ return result
+
+ _columns = {
+ 'cashbox_line_ids' : fields.one2many('account.journal.cashbox.line', 'journal_id', 'CashBox'),
+ }
+
+ _defaults = {
+ 'cashbox_line_ids' : _default_cashbox_line_ids,
+ }
+
+account_journal()
+
+class account_journal_cashbox_line(osv.osv):
+ _name = 'account.journal.cashbox.line'
+ _rec_name = 'value'
+ _columns = {
+ 'pieces': fields.float('Values', digits_compute=dp.get_precision('Account')),
+ 'journal_id' : fields.many2one('account.journal', 'Journal', required=True, select=1),
+ }
+
+ _order = 'pieces asc'
+
+account_journal_cashbox_line()
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml
index 211a40f030f..393c468c952 100644
--- a/addons/account/account_invoice_view.xml
+++ b/addons/account/account_invoice_view.xml
@@ -177,11 +177,11 @@
domain="[('supplier', '=', True)]"/>
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
-
+
-
-
-
+
-
-
-
+
-
-
+
+
+
+
diff --git a/addons/account/res_currency.py b/addons/account/res_currency.py
index 8c9c6a8b103..8e02836274a 100644
--- a/addons/account/res_currency.py
+++ b/addons/account/res_currency.py
@@ -44,4 +44,5 @@ class res_currency_account(osv.osv):
res_currency_account()
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
diff --git a/addons/account/test/account_cash_statement.yml b/addons/account/test/account_cash_statement.yml
index 2f1ae4b0076..6bff530bb54 100644
--- a/addons/account/test/account_cash_statement.yml
+++ b/addons/account/test/account_cash_statement.yml
@@ -1,5 +1,5 @@
-
- In order to test Cash statement I create a Cash statement and confirm it and check it's move created
+ In order to test Cash statement I create a Cash statement and confirm it and check its move created
-
!record {model: account.bank.statement, id: account_bank_statement_1}:
date: !eval time.strftime('%Y-%m-%d')
@@ -57,7 +57,6 @@
- pieces: 500.0
number: 2
subtotal: 1000.0
- balance_end_cash: 1120.0
-
I clicked on Close CashBox button to close the cashbox
diff --git a/addons/account/wizard/__init__.py b/addons/account/wizard/__init__.py
index a78cf12050b..87a053d558c 100644
--- a/addons/account/wizard/__init__.py
+++ b/addons/account/wizard/__init__.py
@@ -64,6 +64,8 @@ import account_report_account_balance
import account_change_currency
+import pos_box;
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/account/wizard/account_chart_view.xml b/addons/account/wizard/account_chart_view.xml
index d7820caa647..553d83fa01c 100644
--- a/addons/account/wizard/account_chart_view.xml
+++ b/addons/account/wizard/account_chart_view.xml
@@ -7,13 +7,18 @@
form
diff --git a/addons/account/wizard/account_move_journal.py b/addons/account/wizard/account_move_journal.py
index c484da72304..66f634bd59f 100644
--- a/addons/account/wizard/account_move_journal.py
+++ b/addons/account/wizard/account_move_journal.py
@@ -102,22 +102,20 @@ class account_move_journal(osv.osv_memory):
period = period_pool.browse(cr, uid, [period_id], ['name'])[0]['name']
period_string = _("Period: %s") % tools.ustr(period)
- separator_string = _("Open Journal Items !")
open_string = _("Open")
view = """
""" % (open_string, separator_string, journal_string, period_string)
+
+ """ % (_('Journal'), journal_string, _('Period'), period_string, open_string)
view = etree.fromstring(view.encode('utf8'))
xarch, xfields = self._view_look_dom_arch(cr, uid, view, view_id, context=context)
diff --git a/addons/account/wizard/account_report_account_balance_view.xml b/addons/account/wizard/account_report_account_balance_view.xml
index b700ab8a736..c42e0bc38ad 100644
--- a/addons/account/wizard/account_report_account_balance_view.xml
+++ b/addons/account/wizard/account_report_account_balance_view.xml
@@ -13,10 +13,6 @@
-
-
-
-
diff --git a/addons/account/wizard/account_report_common_journal.py b/addons/account/wizard/account_report_common_journal.py
index 92076cfe572..ed0f03df5b5 100644
--- a/addons/account/wizard/account_report_common_journal.py
+++ b/addons/account/wizard/account_report_common_journal.py
@@ -26,7 +26,7 @@ class account_common_journal_report(osv.osv_memory):
_description = 'Account Common Journal Report'
_inherit = "account.common.report"
_columns = {
- 'amount_currency': fields.boolean("With Currency", help="Print Report with the currency column if the currency is different then the company currency"),
+ 'amount_currency': fields.boolean("With Currency", help="Print Report with the currency column if the currency differs from the company currency."),
}
def _build_contexts(self, cr, uid, ids, data, context=None):
diff --git a/addons/account/wizard/account_report_common_view.xml b/addons/account/wizard/account_report_common_view.xml
index 2711154a35e..18953270e09 100644
--- a/addons/account/wizard/account_report_common_view.xml
+++ b/addons/account/wizard/account_report_common_view.xml
@@ -17,16 +17,16 @@
-
+
-
-
-
+
+
+
-
-
-
+
+
+
diff --git a/addons/account/wizard/account_report_general_journal_view.xml b/addons/account/wizard/account_report_general_journal_view.xml
index 96f45f9d028..93c82cd5810 100644
--- a/addons/account/wizard/account_report_general_journal_view.xml
+++ b/addons/account/wizard/account_report_general_journal_view.xml
@@ -9,10 +9,6 @@
-
-
-
-
diff --git a/addons/account/wizard/account_report_general_ledger.py b/addons/account/wizard/account_report_general_ledger.py
index d0b3c0fa3d3..8078da372ca 100644
--- a/addons/account/wizard/account_report_general_ledger.py
+++ b/addons/account/wizard/account_report_general_ledger.py
@@ -30,7 +30,7 @@ class account_report_general_ledger(osv.osv_memory):
'landscape': fields.boolean("Landscape Mode"),
'initial_balance': fields.boolean('Include Initial Balances',
help='If you selected to filter by date or period, this field allow you to add a row to display the amount of debit/credit/balance that precedes the filter you\'ve set.'),
- 'amount_currency': fields.boolean("With Currency", help="It adds the currency column if the currency is different then the company currency"),
+ 'amount_currency': fields.boolean("With Currency", help="It adds the currency column on report if the currency differs from the company currency."),
'sortby': fields.selection([('sort_date', 'Date'), ('sort_journal_partner', 'Journal & Partner')], 'Sort by', required=True),
'journal_ids': fields.many2many('account.journal', 'account_report_general_ledger_journal_rel', 'account_id', 'journal_id', 'Journals', required=True),
}
diff --git a/addons/account/wizard/account_report_general_ledger_view.xml b/addons/account/wizard/account_report_general_ledger_view.xml
index 20b6ab0f042..5078eeac88b 100644
--- a/addons/account/wizard/account_report_general_ledger_view.xml
+++ b/addons/account/wizard/account_report_general_ledger_view.xml
@@ -9,10 +9,6 @@
-
-
-
-
diff --git a/addons/account/wizard/account_report_partner_balance_view.xml b/addons/account/wizard/account_report_partner_balance_view.xml
index 6dbe6d7b77b..e8abd1c68a2 100644
--- a/addons/account/wizard/account_report_partner_balance_view.xml
+++ b/addons/account/wizard/account_report_partner_balance_view.xml
@@ -9,10 +9,6 @@
-
-
-
-
diff --git a/addons/account/wizard/account_report_partner_ledger.py b/addons/account/wizard/account_report_partner_ledger.py
index 7a3f3932f1b..60ac20a42ed 100644
--- a/addons/account/wizard/account_report_partner_ledger.py
+++ b/addons/account/wizard/account_report_partner_ledger.py
@@ -34,7 +34,7 @@ class account_partner_ledger(osv.osv_memory):
help='If you selected to filter by date or period, this field allow you to add a row to display the amount of debit/credit/balance that precedes the filter you\'ve set.'),
'filter': fields.selection([('filter_no', 'No Filters'), ('filter_date', 'Date'), ('filter_period', 'Periods'), ('unreconciled', 'Unreconciled Entries')], "Filter by", required=True),
'page_split': fields.boolean('One Partner Per Page', help='Display Ledger Report with One partner per page'),
- 'amount_currency': fields.boolean("With Currency", help="It adds the currency column if the currency is different then the company currency"),
+ 'amount_currency': fields.boolean("With Currency", help="It adds the currency column on report if the currency differs from the company currency."),
'journal_ids': fields.many2many('account.journal', 'account_partner_ledger_journal_rel', 'account_id', 'journal_id', 'Journals', required=True),
}
_defaults = {
diff --git a/addons/account/wizard/account_report_partner_ledger_view.xml b/addons/account/wizard/account_report_partner_ledger_view.xml
index c3c731c1954..1bc223a956d 100644
--- a/addons/account/wizard/account_report_partner_ledger_view.xml
+++ b/addons/account/wizard/account_report_partner_ledger_view.xml
@@ -9,10 +9,6 @@
-
-
-
-
diff --git a/addons/account/wizard/account_report_print_journal_view.xml b/addons/account/wizard/account_report_print_journal_view.xml
index cf4eb0a0c9f..9a2ef25a1fe 100644
--- a/addons/account/wizard/account_report_print_journal_view.xml
+++ b/addons/account/wizard/account_report_print_journal_view.xml
@@ -9,10 +9,6 @@
-
-
-
-
diff --git a/addons/account/wizard/account_subscription_generate.py b/addons/account/wizard/account_subscription_generate.py
index 8efcd25b789..dc5209ebea0 100644
--- a/addons/account/wizard/account_subscription_generate.py
+++ b/addons/account/wizard/account_subscription_generate.py
@@ -28,7 +28,7 @@ class account_subscription_generate(osv.osv_memory):
_name = "account.subscription.generate"
_description = "Subscription Compute"
_columns = {
- 'date': fields.date('Date', required=True),
+ 'date': fields.date('Generate Entries Before', required=True),
}
_defaults = {
'date': lambda *a: time.strftime('%Y-%m-%d'),
diff --git a/addons/account/wizard/account_subscription_generate_view.xml b/addons/account/wizard/account_subscription_generate_view.xml
index 64ca3beeb11..e32439537d7 100644
--- a/addons/account/wizard/account_subscription_generate_view.xml
+++ b/addons/account/wizard/account_subscription_generate_view.xml
@@ -8,8 +8,6 @@
form
diff --git a/addons/hr_recruitment/res_config_view.xml b/addons/hr_recruitment/res_config_view.xml
index 011e3816c2c..60d6d8d152d 100644
--- a/addons/hr_recruitment/res_config_view.xml
+++ b/addons/hr_recruitment/res_config_view.xml
@@ -7,16 +7,15 @@
form
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/addons/hr_recruitment/security/ir.model.access.csv b/addons/hr_recruitment/security/ir.model.access.csv
index 7d082070be5..8b029c9324a 100644
--- a/addons/hr_recruitment/security/ir.model.access.csv
+++ b/addons/hr_recruitment/security/ir.model.access.csv
@@ -3,7 +3,6 @@ access_hr_applicant_user,hr.applicant.user,model_hr_applicant,base.group_hr_user
access_hr_recruitment_report,hr.recruitment.report,model_hr_recruitment_report,base.group_hr_manager,1,1,1,1
access_hr_recruitment_stage_user,hr.recruitment.stage.user,model_hr_recruitment_stage,base.group_hr_user,1,1,1,1
access_hr_recruitment_degree,hr.recruitment.degree,model_hr_recruitment_degree,base.group_hr_user,1,1,1,1
-access_mail_message_user,mail.message.user,mail.model_mail_message,base.group_hr_user,1,1,1,1
access_res_partner_hr_user,res.partner.user,base.model_res_partner,base.group_hr_user,1,1,1,1
access_survey_hr_user,survey.hr.user,survey.model_survey,base.group_hr_user,1,1,1,0
access_crm_meeting_hruser,crm.meeting.hruser,base_calendar.model_crm_meeting,base.group_hr_user,1,1,1,1
diff --git a/addons/hr_timesheet/__openerp__.py b/addons/hr_timesheet/__openerp__.py
index 6b8e618116e..d6a6232d1a0 100644
--- a/addons/hr_timesheet/__openerp__.py
+++ b/addons/hr_timesheet/__openerp__.py
@@ -34,8 +34,8 @@ the analytic account.
Lots of reporting on time and employee tracking are provided.
-It is completely integrated with the cost accounting module. It allows you
-to set up a management by affair.
+It is completely integrated with the cost accounting module. It allows you to set
+up a management by affair.
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py
index 66911f25ff5..53119853c31 100644
--- a/addons/hr_timesheet/hr_timesheet.py
+++ b/addons/hr_timesheet/hr_timesheet.py
@@ -198,6 +198,13 @@ class account_analytic_account(osv.osv):
_columns = {
'use_timesheets': fields.boolean('Timesheets', help="Check this field if this project manages timesheets"),
}
+
+ def on_change_template(self, cr, uid, ids, template_id, context=None):
+ res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
+ if template_id and 'value' in res:
+ template = self.browse(cr, uid, template_id, context=context)
+ res['value']['use_timesheets'] = template.use_timesheets
+ return res
account_analytic_account()
diff --git a/addons/hr_timesheet_invoice/__openerp__.py b/addons/hr_timesheet_invoice/__openerp__.py
index 65b8905fca0..e233158bda3 100644
--- a/addons/hr_timesheet_invoice/__openerp__.py
+++ b/addons/hr_timesheet_invoice/__openerp__.py
@@ -24,12 +24,14 @@
'name': 'Invoice on Timesheets',
'version': '1.0',
'category': 'Sales Management',
- 'description': """Generate your Invoices from Expenses, Timesheet Entries, ...
+ 'description': """
+Generate your Invoices from Expenses, Timesheet Entries.
+========================================================
+
Module to generate invoices based on costs (human resources, expenses, ...).
-============================================================================
You can define price lists in analytic account, make some theoretical revenue
-reports, etc.""",
+reports.""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'images': ['images/hr_bill_task_work.jpeg','images/hr_type_of_invoicing.jpeg'],
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
index f1138d7b246..54257f5efba 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
@@ -64,7 +64,7 @@ class account_analytic_account(osv.osv):
_inherit = "account.analytic.account"
_columns = {
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist',
- help="The product to invoice is defined on the employee form, the price will be deduced by this pricelist on the product."),
+ help="The product to invoice is defined on the employee form, the price will be deducted by this pricelist on the product."),
'amount_max': fields.float('Max. Invoice Price',
help="Keep empty if this contract is not limited to a total fixed price."),
'amount_invoiced': fields.function(_invoiced_calc, string='Invoiced Amount',
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
index 9888972105e..81dc1e3c9cf 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
@@ -114,7 +114,7 @@
form
-
+
diff --git a/addons/hr_timesheet_invoice/wizard/hr_timesheet_invoice_create.py b/addons/hr_timesheet_invoice/wizard/hr_timesheet_invoice_create.py
index 30edae72d9a..ea4d286c89f 100644
--- a/addons/hr_timesheet_invoice/wizard/hr_timesheet_invoice_create.py
+++ b/addons/hr_timesheet_invoice/wizard/hr_timesheet_invoice_create.py
@@ -179,7 +179,7 @@ class hr_timesheet_invoice_create(osv.osv_memory):
'time': fields.boolean('Time spent', help='The time of each work done will be displayed on the invoice'),
'name': fields.boolean('Description', help='The detail of each work done will be displayed on the invoice'),
'price': fields.boolean('Cost', help='The cost of each work done will be displayed on the invoice. You probably don\'t want to check this'),
- 'product': fields.many2one('product.product', 'Product', help='Complete this field only if you want to force to use a specific product. Keep empty to use the real product that comes from the cost.'),
+ 'product': fields.many2one('product.product', 'Product', help='Fill this field only if you want to force to use a specific product. Keep empty to use the real product that comes from the cost.'),
}
_defaults = {
diff --git a/addons/hr_timesheet_sheet/__openerp__.py b/addons/hr_timesheet_sheet/__openerp__.py
index defa3ee5873..cc788d95a83 100644
--- a/addons/hr_timesheet_sheet/__openerp__.py
+++ b/addons/hr_timesheet_sheet/__openerp__.py
@@ -25,26 +25,27 @@
'version': '1.0',
'category': 'Human Resources',
"sequence": 16,
+ "summary": "Attendances, Activities, Timing",
'description': """
This module helps you to easily encode and validate timesheet and attendances within the same view.
===================================================================================================
-The upper part of the view is for attendances and track (sign in/sign out) events.
-The lower part is for timesheet.
+ * It will maintain attendances and track (sign in/sign out) events.
+ * Track the timesheet lines.
Other tabs contains statistics views to help you analyse your
time or the time of your team:
-* Time spent by day (with attendances)
-* Time spent by project
+ * Time spent by day (with attendances)
+ * Time spent by project
This module also implements a complete timesheet validation process:
-* Draft sheet
-* Confirmation at the end of the period by the employee
-* Validation by the project manager
+ * Draft sheet
+ * Confirmation at the end of the period by the employee
+ * Validation by the project manager
The validation can be configured in the company:
-* Period size (day, week, month, year)
-* Maximal difference between timesheet and attendances
+ * Period size (day, week, month, year)
+ * Maximal difference between timesheet and attendances
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
index e66694223d2..c5839f886d3 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
@@ -1,6 +1,15 @@
+
+
+
+
+ Module Timesheets Validation has been installed
+ From the top menu "Human Resources", encode and validate
+ timesheets and attendances.
+
+
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
index 6635c38b909..dbc9e1d6dac 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
@@ -200,12 +200,12 @@
form
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/addons/hr_timesheet_sheet/i18n/sv.po b/addons/hr_timesheet_sheet/i18n/sv.po
index 341abbb0858..ba584de0934 100644
--- a/addons/hr_timesheet_sheet/i18n/sv.po
+++ b/addons/hr_timesheet_sheet/i18n/sv.po
@@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.14\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2010-11-22 21:07+0000\n"
-"Last-Translator: Olivier Dony (OpenERP) \n"
+"PO-Revision-Date: 2012-07-16 15:49+0000\n"
+"Last-Translator: Anders Wallenquist \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:05+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
+"X-Launchpad-Export-Date: 2012-07-17 04:49+0000\n"
+"X-Generator: Launchpad (build 15627)\n"
#. module: hr_timesheet_sheet
#: field:hr.analytic.timesheet,sheet_id:0 field:hr.attendance,sheet_id:0
@@ -66,7 +66,7 @@ msgstr ""
#. module: hr_timesheet_sheet
#: view:hr_timesheet_sheet.sheet:0
msgid "Today"
-msgstr "Today"
+msgstr "Idag"
#. module: hr_timesheet_sheet
#: code:addons/hr_timesheet_sheet/hr_timesheet_sheet.py:274
@@ -74,7 +74,8 @@ msgstr "Today"
msgid ""
"Please verify that the total difference of the sheet is lower than %.2f !"
msgstr ""
-"Please verify that the total difference of the sheet is lower than %.2f !"
+"Vänligen kontrollera att den totala skillnaden mellan rapporterna är mindre "
+"än %.2f!"
#. module: hr_timesheet_sheet
#: selection:hr.timesheet.report,month:0 selection:timesheet.report,month:0
@@ -89,7 +90,7 @@ msgstr "# Kostnad"
#. module: hr_timesheet_sheet
#: view:hr.timesheet.report:0 view:timesheet.report:0
msgid "Timesheet of last month"
-msgstr ""
+msgstr "Tidrapport från förra månaden"
#. module: hr_timesheet_sheet
#: view:hr.timesheet.report:0 field:hr.timesheet.report,company_id:0
diff --git a/addons/idea/__openerp__.py b/addons/idea/__openerp__.py
index 3a9a44c6436..7ee4a72de05 100644
--- a/addons/idea/__openerp__.py
+++ b/addons/idea/__openerp__.py
@@ -25,8 +25,8 @@
'version': '0.1',
'category': 'Tools',
'description': """
-This module allows your user to easily and efficiently participate in enterprise innovation.
-============================================================================================
+This module allows user to easily and efficiently participate in enterprise innovation.
+=======================================================================================
It allows everybody to express ideas about different subjects.
Then, other users can comment on these ideas and vote for particular ideas.
diff --git a/addons/import_base/__openerp__.py b/addons/import_base/__openerp__.py
index 7e4334f4a1d..294f864a0e3 100644
--- a/addons/import_base/__openerp__.py
+++ b/addons/import_base/__openerp__.py
@@ -20,12 +20,12 @@
##############################################################################
{
- 'name': 'Framework for complex import',
+ 'name': 'Framework for Complex Import',
'version': '0.9',
'category': 'Hidden/Dependency',
'description': """
- This module provide a class import_framework to help importing
- complex data from other software
+This module provide a class import_framework to help importing complex data from
+other software.
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
diff --git a/addons/import_google/__openerp__.py b/addons/import_google/__openerp__.py
index 5242633747f..9e47a213072 100644
--- a/addons/import_google/__openerp__.py
+++ b/addons/import_google/__openerp__.py
@@ -23,7 +23,7 @@
'name': 'Google Import',
'version': '1.0',
'category': 'Customer Relationship Management',
- 'description': """The module adds google contact in partner address and add google calendar events details in Meeting""",
+ 'description': """The module adds google contact in partner address and add google calendar events details in Meeting.""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['base', 'import_base', 'google_base_account', 'base_calendar'],
diff --git a/addons/import_google/i18n/es.po b/addons/import_google/i18n/es.po
new file mode 100644
index 00000000000..325df15688c
--- /dev/null
+++ b/addons/import_google/i18n/es.po
@@ -0,0 +1,217 @@
+# Spanish translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-07-21 21:08+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Spanish \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-22 04:45+0000\n"
+"X-Generator: Launchpad (build 15654)\n"
+
+#. module: import_google
+#: help:synchronize.google.import,group_name:0
+msgid "Choose which group to import, By default it takes all."
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Import Google Calendar Events"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "_Import Events"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:71
+#, python-format
+msgid ""
+"No Google Username or password Defined for user.\n"
+"Please define in user view"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:127
+#, python-format
+msgid ""
+"Invalid login detail !\n"
+" Specify Username/Password."
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,supplier:0
+msgid "Supplier"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Import Options"
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,group_name:0
+msgid "Group Name"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_crm_case_categ
+msgid "Category of Case"
+msgstr ""
+
+#. module: import_google
+#: model:ir.actions.act_window,name:import_google.act_google_login_contact_form
+#: model:ir.ui.menu,name:import_google.menu_sync_contact
+msgid "Import Google Contacts"
+msgstr ""
+
+#. module: import_google
+#: view:google.import.message:0
+msgid "Import Google Data"
+msgstr ""
+
+#. module: import_google
+#: view:crm.meeting:0
+msgid "Meeting Type"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google.py:38
+#: code:addons/import_google/wizard/import_google_data.py:28
+#, python-format
+msgid ""
+"Please install gdata-python-client from http://code.google.com/p/gdata-"
+"python-client/downloads/list"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_google_login
+msgid "Google Contact"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Import contacts from a google account"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:133
+#, python-format
+msgid "Please specify correct user and password !"
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,customer:0
+msgid "Customer"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "_Cancel"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_synchronize_google_import
+msgid "synchronize.google.import"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "_Import Contacts"
+msgstr ""
+
+#. module: import_google
+#: model:ir.actions.act_window,name:import_google.act_google_login_form
+#: model:ir.ui.menu,name:import_google.menu_sync_calendar
+msgid "Import Google Calendar"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:50
+#, python-format
+msgid "Import google"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:127
+#: code:addons/import_google/wizard/import_google_data.py:133
+#, python-format
+msgid "Error"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:71
+#, python-format
+msgid "Warning !"
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,create_partner:0
+msgid "Options"
+msgstr ""
+
+#. module: import_google
+#: view:google.import.message:0
+msgid "_Ok"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google.py:38
+#: code:addons/import_google/wizard/import_google_data.py:28
+#, python-format
+msgid "Google Contacts Import Error!"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_google_import_message
+msgid "Import Message"
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,calendar_name:0
+msgid "Calendar Name"
+msgstr ""
+
+#. module: import_google
+#: help:synchronize.google.import,supplier:0
+msgid "Check this box to set newly created partner as Supplier."
+msgstr ""
+
+#. module: import_google
+#: selection:synchronize.google.import,create_partner:0
+msgid "Import only address"
+msgstr ""
+
+#. module: import_google
+#: field:crm.case.categ,user_id:0
+msgid "User"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Partner Status for this Group:"
+msgstr ""
+
+#. module: import_google
+#: field:google.import.message,name:0
+msgid "Message"
+msgstr ""
+
+#. module: import_google
+#: selection:synchronize.google.import,create_partner:0
+msgid "Create partner for each contact"
+msgstr ""
+
+#. module: import_google
+#: help:synchronize.google.import,customer:0
+msgid "Check this box to set newly created partner as Customer."
+msgstr ""
diff --git a/addons/import_google/i18n/pt_BR.po b/addons/import_google/i18n/pt_BR.po
new file mode 100644
index 00000000000..56654d0f9e7
--- /dev/null
+++ b/addons/import_google/i18n/pt_BR.po
@@ -0,0 +1,217 @@
+# Brazilian Portuguese translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-07-16 20:10+0000\n"
+"Last-Translator: Rafael \n"
+"Language-Team: Brazilian Portuguese \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-17 04:49+0000\n"
+"X-Generator: Launchpad (build 15627)\n"
+
+#. module: import_google
+#: help:synchronize.google.import,group_name:0
+msgid "Choose which group to import, By default it takes all."
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Import Google Calendar Events"
+msgstr "Importar o Calendário de Eventos da Google"
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "_Import Events"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:71
+#, python-format
+msgid ""
+"No Google Username or password Defined for user.\n"
+"Please define in user view"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:127
+#, python-format
+msgid ""
+"Invalid login detail !\n"
+" Specify Username/Password."
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,supplier:0
+msgid "Supplier"
+msgstr "Fornecedor"
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Import Options"
+msgstr "Opções de importação"
+
+#. module: import_google
+#: field:synchronize.google.import,group_name:0
+msgid "Group Name"
+msgstr "Nome do Grupo"
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_crm_case_categ
+msgid "Category of Case"
+msgstr ""
+
+#. module: import_google
+#: model:ir.actions.act_window,name:import_google.act_google_login_contact_form
+#: model:ir.ui.menu,name:import_google.menu_sync_contact
+msgid "Import Google Contacts"
+msgstr "Importar Contatos do Google"
+
+#. module: import_google
+#: view:google.import.message:0
+msgid "Import Google Data"
+msgstr ""
+
+#. module: import_google
+#: view:crm.meeting:0
+msgid "Meeting Type"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google.py:38
+#: code:addons/import_google/wizard/import_google_data.py:28
+#, python-format
+msgid ""
+"Please install gdata-python-client from http://code.google.com/p/gdata-"
+"python-client/downloads/list"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_google_login
+msgid "Google Contact"
+msgstr ""
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Import contacts from a google account"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:133
+#, python-format
+msgid "Please specify correct user and password !"
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,customer:0
+msgid "Customer"
+msgstr "Cliente"
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "_Cancel"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_synchronize_google_import
+msgid "synchronize.google.import"
+msgstr "sincronizar.importação.google"
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "_Import Contacts"
+msgstr ""
+
+#. module: import_google
+#: model:ir.actions.act_window,name:import_google.act_google_login_form
+#: model:ir.ui.menu,name:import_google.menu_sync_calendar
+msgid "Import Google Calendar"
+msgstr "Importar Calendário do Google"
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:50
+#, python-format
+msgid "Import google"
+msgstr ""
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:127
+#: code:addons/import_google/wizard/import_google_data.py:133
+#, python-format
+msgid "Error"
+msgstr "Erro"
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google_data.py:71
+#, python-format
+msgid "Warning !"
+msgstr "Aviso !"
+
+#. module: import_google
+#: field:synchronize.google.import,create_partner:0
+msgid "Options"
+msgstr "Opções"
+
+#. module: import_google
+#: view:google.import.message:0
+msgid "_Ok"
+msgstr "_Ok"
+
+#. module: import_google
+#: code:addons/import_google/wizard/import_google.py:38
+#: code:addons/import_google/wizard/import_google_data.py:28
+#, python-format
+msgid "Google Contacts Import Error!"
+msgstr ""
+
+#. module: import_google
+#: model:ir.model,name:import_google.model_google_import_message
+msgid "Import Message"
+msgstr ""
+
+#. module: import_google
+#: field:synchronize.google.import,calendar_name:0
+msgid "Calendar Name"
+msgstr "Nome do Calendário"
+
+#. module: import_google
+#: help:synchronize.google.import,supplier:0
+msgid "Check this box to set newly created partner as Supplier."
+msgstr ""
+
+#. module: import_google
+#: selection:synchronize.google.import,create_partner:0
+msgid "Import only address"
+msgstr ""
+
+#. module: import_google
+#: field:crm.case.categ,user_id:0
+msgid "User"
+msgstr "Usuário"
+
+#. module: import_google
+#: view:synchronize.google.import:0
+msgid "Partner Status for this Group:"
+msgstr ""
+
+#. module: import_google
+#: field:google.import.message,name:0
+msgid "Message"
+msgstr "Mensagem"
+
+#. module: import_google
+#: selection:synchronize.google.import,create_partner:0
+msgid "Create partner for each contact"
+msgstr ""
+
+#. module: import_google
+#: help:synchronize.google.import,customer:0
+msgid "Check this box to set newly created partner as Customer."
+msgstr ""
diff --git a/addons/import_sugarcrm/__openerp__.py b/addons/import_sugarcrm/__openerp__.py
index ea23247761d..f84b096e28a 100644
--- a/addons/import_sugarcrm/__openerp__.py
+++ b/addons/import_sugarcrm/__openerp__.py
@@ -23,8 +23,9 @@
'name': 'SugarCRM Import',
'version': '1.0',
'category': 'Customer Relationship Management',
- 'description': """This Module Import SugarCRM "Leads", "Opportunities", "Users", "Accounts",
- "Contacts", "Employees", Meetings, Phonecalls, Emails, and Project, Project Tasks Data into OpenERP Module.""",
+ 'description': """
+This Module Import SugarCRM Leads, Opportunities, Users, Accounts, Contacts, Employees,
+Meetings, Phonecalls, Emails, Project and Project Tasks Data into OpenERP Module.""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['import_base','crm', 'document'],
diff --git a/addons/import_sugarcrm/import_sugarcrm_view.xml b/addons/import_sugarcrm/import_sugarcrm_view.xml
index 7188108ba6c..6ef9be1a5bd 100644
--- a/addons/import_sugarcrm/import_sugarcrm_view.xml
+++ b/addons/import_sugarcrm/import_sugarcrm_view.xml
@@ -13,7 +13,7 @@
+ type="object"/> or
diff --git a/addons/knowledge/__openerp__.py b/addons/knowledge/__openerp__.py
index dca620b0f07..f6b042f364b 100644
--- a/addons/knowledge/__openerp__.py
+++ b/addons/knowledge/__openerp__.py
@@ -21,7 +21,7 @@
{
- "name" : "Document Management System",
+ "name" : "Knowledge Management System",
"version" : "1.0",
"depends" : ["base","base_setup"],
"author" : "OpenERP SA",
diff --git a/addons/knowledge/i18n/nb.po b/addons/knowledge/i18n/nb.po
new file mode 100644
index 00000000000..6f721f133bb
--- /dev/null
+++ b/addons/knowledge/i18n/nb.po
@@ -0,0 +1,33 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 01:37+0100\n"
+"PO-Revision-Date: 2012-07-23 10:30+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-24 04:52+0000\n"
+"X-Generator: Launchpad (build 15668)\n"
+
+#. module: knowledge
+#: model:ir.ui.menu,name:knowledge.menu_document2
+msgid "Collaborative Content"
+msgstr ""
+
+#. module: knowledge
+#: model:ir.ui.menu,name:knowledge.menu_document_configuration
+msgid "Configuration"
+msgstr "Konfigurasjon"
+
+#. module: knowledge
+#: model:ir.ui.menu,name:knowledge.menu_document
+msgid "Knowledge"
+msgstr "Kunnskap"
diff --git a/addons/knowledge/knowledge_view.xml b/addons/knowledge/knowledge_view.xml
index cdc176b347f..9acba0f4b43 100644
--- a/addons/knowledge/knowledge_view.xml
+++ b/addons/knowledge/knowledge_view.xml
@@ -1,13 +1,18 @@
+
-
-
-
+
-
+
+
-
+
+
+
+
+
diff --git a/addons/knowledge/res_config_view.xml b/addons/knowledge/res_config_view.xml
index 0cec5899c41..ec4d91683f1 100644
--- a/addons/knowledge/res_config_view.xml
+++ b/addons/knowledge/res_config_view.xml
@@ -11,7 +11,6 @@
or
-
diff --git a/addons/l10n_ar/__init__.py b/addons/l10n_ar/__init__.py
new file mode 100644
index 00000000000..92da5dbf966
--- /dev/null
+++ b/addons/l10n_ar/__init__.py
@@ -0,0 +1,32 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (c) 2011 Cubic ERP - Teradata SAC. (http://cubicerp.com).
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# 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 2
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_ar/__openerp__.py b/addons/l10n_ar/__openerp__.py
new file mode 100644
index 00000000000..8971ce94b22
--- /dev/null
+++ b/addons/l10n_ar/__openerp__.py
@@ -0,0 +1,51 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 Cubic ERP - Teradata SAC ().
+#
+# 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": "Argentina Localization Chart Account",
+ "version": "1.0",
+ "description": """
+Argentinian accounting chart and tax localization.
+
+Plan contable argentino e impuestos de acuerdo a disposiciones vigentes
+
+ """,
+ "author": ["Cubic ERP"],
+ "website": "http://cubicERP.com",
+ "category": "Localization/Account Charts",
+ "depends": [
+ "account_chart",
+ ],
+ "data":[
+ "account_tax_code.xml",
+ "l10n_ar_chart.xml",
+ "account_tax.xml",
+ "l10n_ar_wizard.xml",
+ ],
+ "demo_xml": [
+ ],
+ "update_xml": [
+ ],
+ "active": False,
+ "installable": True,
+ "certificate" : "",
+ 'images': ['images/config_chart_l10n_ar.jpeg','images/l10n_ar_chart.jpeg'],
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_ar/account_tax.xml b/addons/l10n_ar/account_tax.xml
new file mode 100644
index 00000000000..8c8c648f0af
--- /dev/null
+++ b/addons/l10n_ar/account_tax.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+ IVA 21% Venta
+ 0.210000
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+ IVA 21% Compra
+ 0.210000
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
+ IVA 27% Venta
+ 0.270000
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+ IVA 27% Compra
+ 0.270000
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
+ IVA 10.5% Venta
+ 0.105000
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+ IVA 10.5% Compra
+ 0.105000
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
+ Percepción IVA 2%
+ 0.02000
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_ar/account_tax_code.xml b/addons/l10n_ar/account_tax_code.xml
new file mode 100644
index 00000000000..806a358f7cc
--- /dev/null
+++ b/addons/l10n_ar/account_tax_code.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+ Argentina Impuestos
+
+
+ Base Imponible
+
+
+
+ Base Imponible - Ventas
+
+
+
+ Ventas Gravadas con IVA
+
+
+
+ Ventas NO Gravadas (Exoneradas)
+
+
+
+ Ventas Gravadas Fuera de Ámbito
+
+
+
+ Base Imponible - Compras
+
+
+
+ Compras Gravadas con IVA
+
+
+
+ Compras NO Gravadas (Exoneradas)
+
+
+
+ Compras Gravadas Fuera de Ámbito
+
+
+
+
+ Impuesto General a las Ventas (IVA) Total a Pagar
+
+
+
+ Impuesto Pagado
+
+
+
+ Impuesto Pagado IVA
+
+ -1
+
+
+ Impuesto Pagado de Exonerados al IVA
+
+ -1
+
+
+ Impuesto Pagado Fuera de Ámbito
+
+ -1
+
+
+ Impuesto Cobrado
+
+
+
+ Impuesto Cobrado IVA
+
+
+
+ Impuesto Cobrado de Exonerados al IVA
+
+
+
+ Impuesto Cobrado Fuera de Ámbito
+
+
+
+
+ Impuesto Nacionales a Pagar
+
+
+
+ Impuesto a las Ganancias a Pagar
+
+
+
+ Impuesto a los Bienes Personales a Pagar
+
+
+
+ Impuesto a la Ganancia Mínima Presunta a Pagar
+
+
+
+ Monotributo a Pagar
+
+
+
+
diff --git a/addons/l10n_ar/i18n/es.po b/addons/l10n_ar/i18n/es.po
new file mode 100644
index 00000000000..097598864fe
--- /dev/null
+++ b/addons/l10n_ar/i18n/es.po
@@ -0,0 +1,72 @@
+# Spanish translation for openobject-addons
+# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2011-01-11 11:15+0000\n"
+"PO-Revision-Date: 2011-01-10 13:46+0000\n"
+"Last-Translator: Yury Tello \n"
+"Language-Team: Spanish \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2011-01-15 05:56+0000\n"
+"X-Generator: Launchpad (build 12177)\n"
+
+#. module: l10n_ar
+#: model:ir.module.module,description:l10n_ar.module_meta_information
+msgid ""
+"\n"
+" Argentinian Accounting : chart of Account\n"
+" "
+msgstr ""
+"\n"
+" Contabilidad Peruana : Plan de cuentas\n"
+" "
+
+#. module: l10n_ar
+#: model:ir.module.module,shortdesc:l10n_ar.module_meta_information
+msgid "Argentinian Chart of Account"
+msgstr "Plan de cuentas de Argentina"
+
+#. module: l10n_ar
+#: model:ir.actions.todo,note:l10n_ar.config_call_account_template_in_minimal
+msgid ""
+"Generate Chart of Accounts from a Chart Template. You will be asked to pass "
+"the name of the company, the chart template to follow, the no. of digits to "
+"generate the code for your accounts and Bank account, currency to create "
+"Journals. Thus,the pure copy of chart Template is generated.\n"
+"\tThis is the same wizard that runs from Financial "
+"Management/Configuration/Financial Accounting/Financial Accounts/Generate "
+"Chart of Accounts from a Chart Template."
+msgstr ""
+"Generar el plan contable a partir de una plantilla de plan contable. Se le "
+"pedirá el nombre de la compañia, la plantilla de plan contable a utilizar, "
+"el número de dígitos para generar el código de las cuentas y de la cuenta "
+"bancaria, la moneda para crear los diarios. Así pues, se genere una copia "
+"exacta de la plantilla de plan contable.\n"
+"\tEste es el mismo asistente que se ejecuta desde Contabilidad y finanzas / "
+"Configuración / Contabilidad financiera / Cuentas financieras / Generar el "
+"plan contable a partir de una plantilla de plan contable."
+
+#~ msgid "Liability"
+#~ msgstr "Pasivo"
+
+#~ msgid "Asset"
+#~ msgstr "Activo"
+
+#~ msgid "Closed"
+#~ msgstr "Cerrado"
+
+#~ msgid "Income"
+#~ msgstr "Ingreso"
+
+#~ msgid "Expense"
+#~ msgstr "Gasto"
+
+#~ msgid "View"
+#~ msgstr "Vista"
diff --git a/addons/l10n_ar/i18n/es_AR.po b/addons/l10n_ar/i18n/es_AR.po
new file mode 100644
index 00000000000..2260a43cf74
--- /dev/null
+++ b/addons/l10n_ar/i18n/es_AR.po
@@ -0,0 +1,54 @@
+# Spanish (Paraguay) translation for openobject-addons
+# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2011.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2011-01-11 11:15+0000\n"
+"PO-Revision-Date: 2011-03-21 16:23+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Spanish (Paraguay) \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2011-03-22 04:36+0000\n"
+"X-Generator: Launchpad (build 12559)\n"
+
+#. module: l10n_ar
+#: model:ir.module.module,description:l10n_ar.module_meta_information
+msgid ""
+"\n"
+" Argentinian Accounting : chart of Account\n"
+" "
+msgstr ""
+"\n"
+" Contabilidad Argentina : Plan de cuentas\n"
+" "
+
+#. module: l10n_ar
+#: model:ir.module.module,shortdesc:l10n_ar.module_meta_information
+msgid "Argentinian Chart of Account"
+msgstr "Plan de cuentas de la Argentina"
+
+#. module: l10n_ar
+#: model:ir.actions.todo,note:l10n_ar.config_call_account_template_in_minimal
+msgid ""
+"Generate Chart of Accounts from a Chart Template. You will be asked to pass "
+"the name of the company, the chart template to follow, the no. of digits to "
+"generate the code for your accounts and Bank account, currency to create "
+"Journals. Thus,the pure copy of chart Template is generated.\n"
+"\tThis is the same wizard that runs from Financial "
+"Management/Configuration/Financial Accounting/Financial Accounts/Generate "
+"Chart of Accounts from a Chart Template."
+msgstr ""
+"Generar el plan contable a partir de una plantilla de plan contable. Se le "
+"pedirá el nombre de la compañía, la plantilla de plan contable a utilizar, "
+"el número de dígitos para generar el código de las cuentas y de la cuenta "
+"bancaria, la moneda para crear los diarios. Así pues, se genere una copia "
+"exacta de la plantilla de plan contable.\n"
+"\tEste es el mismo asistente que se ejecuta desde Contabilidad y finanzas / "
+"Configuración / Contabilidad financiera / Cuentas financieras / Generar el "
+"plan contable a partir de una plantilla de plan contable."
diff --git a/addons/l10n_ar/i18n/l10n_ar.pot b/addons/l10n_ar/i18n/l10n_ar.pot
new file mode 100644
index 00000000000..8d117342eea
--- /dev/null
+++ b/addons/l10n_ar/i18n/l10n_ar.pot
@@ -0,0 +1,35 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * l10n_ar
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 6.1.0-rc1\n"
+"Report-Msgid-Bugs-To: soporte@cubicerp.com\n"
+"POT-Creation-Date: 2011-01-11 11:15:31+0000\n"
+"PO-Revision-Date: 2011-01-11 11:15:31+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: l10n_ar
+#: model:ir.module.module,description:l10n_ar.module_meta_information
+msgid "\n"
+" Argentinian Accounting : chart of Account\n"
+" "
+msgstr ""
+
+#. module: l10n_ar
+#: model:ir.module.module,shortdesc:l10n_ar.module_meta_information
+msgid "Argentinian Chart of Account"
+msgstr ""
+
+#. module: l10n_ar
+#: model:ir.actions.todo,note:l10n_ar.config_call_account_template_in_minimal
+msgid "Generate Chart of Accounts from a Chart Template. You will be asked to pass the name of the company, the chart template to follow, the no. of digits to generate the code for your accounts and Bank account, currency to create Journals. Thus,the pure copy of chart Template is generated.\n"
+" This is the same wizard that runs from Financial Management/Configuration/Financial Accounting/Financial Accounts/Generate Chart of Accounts from a Chart Template."
+msgstr ""
+
diff --git a/addons/l10n_ar/l10n_ar_chart.xml b/addons/l10n_ar/l10n_ar_chart.xml
new file mode 100644
index 00000000000..062697d5aa6
--- /dev/null
+++ b/addons/l10n_ar/l10n_ar_chart.xml
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+ noneviewVistanone
+
+ balanceBG_ACC_10Caja y Bancosasset
+ detailBG_ACC_20Inversionesasset
+ unreconciledBG_ACC_30Créditos por Ventasasset
+ unreconciledBG_ACC_50Otros Créditosasset
+ balanceBG_ACC_60Bienes de Cambioasset
+
+ detailBG_ACN_10Otros Créditos No Corrientesasset
+ detailBG_ACN_40Inversiones Permanentesasset
+ balanceBG_ACN_50Bienes de Usoasset
+
+ unreconciledBG_PAC_10Deudas Bancarias y Financierasliability
+ unreconciledBG_PAC_20Cuentas por Pagarliability
+ unreconciledBG_PAC_35Remuneraciones y Cargas Socialesliability
+ unreconciledBG_PAC_40Cargas Fiscalesliability
+ unreconciledBG_PAC_45Otros Pasivosliability
+
+ unreconciledBG_PAN_10Deudas Bancarias y Financieras a Largo Plazoliability
+ unreconciledBG_PAN_20Otros Pasivos a Largo Plazoliability
+ unreconciledBG_PAN_40Previsionesliability
+
+ balanceBG_PTN_10Patrimonio Netoliability
+
+ noneEGP_FU_010Ventas Netas de Bienes y Serviciosincome
+ noneEGP_FU_030Costo Mercaderías y Servicios Vendidosexpense
+ noneEGP_FU_040Gastos de Administraciónexpense
+ noneEGP_FU_050Gastos de Comercializaciónexpense
+ unreconciledEGP_FU_060Ingresos Financieros y por tenenciaincome
+ noneEGP_FU_070Gastos Financieros y por tenenciaexpense
+ noneEGP_FU_080Otros Ingresosincome
+ noneEGP_FU_090Otros Gastosexpense
+ noneEGP_FU_120Impuesto a las Gananciasexpense
+ noneEGP_FU_160Ganancia (Pérdida) Neta del Ejercicioincome
+
+ noneEGP_NA_010Compras de Bienes de Usonone
+
+ noneORDCuentas de Ordennone
+
+ noneNCLASIFICADOCuentas No Clasificadasnone
+
+
+
+
+ Argentina
+ pcge
+
+ view
+
+
+ Cuentas Patrimoniales
+ .1.BG
+
+
+ view
+
+
+ ACTIVO1view
+ Caja y Bancos11view
+ Caja y Bancos - Caja111view
+ Caja y bancos - Caja / efectivo ARS111.001liquidity
+ Caja y Bancos - Moneda Extranjera112view
+ Caja y bancos - Caja / efectivo USD112.001liquidity
+ Caja y Bancos - Fondos fijos113view
+ Caja y ...- Fondos fijos / caja chica 01 ARS113.001liquidity
+ Caja y Bancos - Cuentas Corrientes114view
+ Caja y Bancos.../ BCO. CTA CTE ARS114.001liquidity
+ Caja y bancos - Valores a Depositar 115other
+ Caja y bancos - Recaudaciones a Depositar 116other
+ Créditos por Ventas12view
+ Créditos por Ventas / Deudores por Ventas121receivable1
+ Créditos por Ventas / Deudores Morosos122receivable1
+ Créditos por Ventas / Deudores en Gestión Judicial123receivable
+ Créditos por Ventas / Deudores Varios124receivable
+ Créditos por Ventas / (-) Previsión para Ds. Incobrables125receivable1
+ Otros Créditos13view
+ Otros Créditos / Préstamos otorgados131receivable
+ Otros Créditos / Anticipos a Proveedores132receivable
+ Otros Créditos / Anticipo de Impuestos133receivable
+ Otros Créditos / Anticipo al Personal134receivable
+ Otros Créditos / Alquileres Pagados por Adelantado135receivable
+ Otros Créditos / Intereses Pagados por Adelantado136receivable
+ Otros Créditos / Accionistas137receivable
+ Otros Créditos / (-) Previsión para Descuentos138receivable
+ Otros Créditos / (-) Intereses (+) a Devengar139receivable
+ Inversiones14view
+ Inversiones / Acciones Transitorias141other
+ Inversiones / Acciones Permanentes142other
+ Inversiones / Títulos Públicos143other
+ Inversiones / (-) Previsión para Devalorización de Acciones144other
+ Bienes de Cambio15view
+ Bienes de Cambio - Mercaderías151view
+ Bienes de Cambio - Mercaderías / Categoria de productos 01151.01other
+ Bienes de Cambio - Mercaderías en Tránsito152other
+ Materias primas153other
+ Productos en Curso de Elaboración154other
+ Productos Elaborados155other
+ Materiales Varios 156other
+ (-) Previsión para Desvalorización de Bienes de Cambio157other
+ Bienes de Uso16view
+ Bienes de Uso / Inmuebles161other
+ Bienes de Uso / Maquinaria162other
+ Bienes de Uso / Equipos163other
+ Bienes de Uso / Rodados164other
+ Bienes de Uso / (-) Depreciación Acumulada165other
+ Bienes Inmateriales17view
+ Bienes Inmateriales / Marcas de Fábrica171other
+ Bienes Inmateriales / Concesiones y Franquicias172other
+ Bienes Inmateriales / Patentes de Invención173other
+ Bienes Inmateriales / (-) Amortización Acumulada174other
+ PASIVO2view
+ Deudas Comerciales21view
+ Deudas Comerciales / Proveedores211payable1
+ Deudas Comerciales / Anticipos de Clientes212payable1
+ Deudas Comerciales / (-) Intereses a Devengar por Compras al Crédito213payable1
+ Deudas Bancarias y Financieras22view
+ Deudas Bancarias y Financieras / Adelantos en Cuenta Corriente221payable
+ Deudas Bancarias y Financieras / Prestamos222payable
+ Deudas Bancarias y Financieras / Obligaciones a Pagar223payable
+ Deudas Bancarias y Financieras / Intereses a Pagar224payable
+ Deudas Bancarias y Financieras / Debentures Emitidos225payable
+ Deudas Fiscales23view
+ Deudas Fiscales / IVA a Pagar231other
+ Deudas Fiscales / Impuesto a los Débitos y Créditos Bancarios a Pagar232other
+ Deudas Fiscales / Impuesto a las Ganancias a Pagar233other
+ Deudas Fiscales / Impuesto sobre los Bienes Personales a Pagar234other
+ Deudas Fiscales / Impuesto a la Ganancia Mínima Presunta a Pagar235other
+ Deudas Fiscales / Monotributo a Pagar236other
+ Deudas Sociales24view
+ Deudas Sociales / Sueldos a Pagar241payable
+ Deudas Sociales / Cargas Sociales a Pagar242payable
+ Deudas Sociales / Provisión para Sueldo Anual Complementario243payable
+ Deudas Sociales / Retenciones a Depositar244payable
+ Otras Deudas25view
+ Otras Deudas / Acreedores Varios251payable
+ Otras Deudas / Dividendos a Pagar252payable
+ Otras Deudas / Cobros por Adelantado253payable
+ Otras Deudas / Honorarios Directores y Síndicos a Pagar254payable
+ Previsiones26view
+ Previsiones / Previsión Indemnización por Despidos261payable
+ Previsiones / Previsión para juicios Pendientes262payable
+ Previsiones / Previsión para Garantías por Service263payable
+ PATRIMONIO NETO3view
+ Capital Social31view
+ Capital social / Capital Suscripto311other
+ Capital social / Acciones en Circulación312other
+ Capital social / Dividendos a Distribuir en Acciones313other
+ Capital social / (-) Descuento de Emisión de Acciones314other
+ Aportes No Capitalizados32view
+ Aportes No Capitalizados / Primas de Emsión321other
+ Aportes No Capitalizados / Aportes Irrevocables Futura Suscripción de Acciones322other
+ Ajustes al Patrimonio33view
+ Ajustes al Patrimonio / Revaluo Técnico de Bienes de Uso331other
+ Ganancias Reservadas34view
+ Reserva Legal341other
+ Reserva Estatutaria342other
+ Reserva Facultativa343other
+ Reserva para Renovación de Bienes de Uso344other
+ Resultados No Asignados35view
+ Resultados Acumulados351other
+ Resultados Acumulados del Ejercicio Anterior352other
+ Ganancias y Pérdidas del Ejercicio353other
+ Resultado del Ejercicio354other
+
+
+ Cuentas de Resultado
+ .2.GP
+
+
+ view
+
+
+ RESULTADOS POSITIVOS4view
+ Resultados Positivos Ordinarios41view
+ Resultados Positivos Ordinarios411view
+ Ventas - Categoria de productos 01411.01other
+ Intereses gananados, obtenidos, percibidos412other
+ Alquileres gananados, obtenidos, percibidos413other
+ Comisiones gananados, obtenidos, percibidos414other
+ Descuentos gananados, obtenidos, percibidos415other
+ Renta de Títulos Públicos416other
+ Honorarios gananados, obtenidos, percibidos417other
+ Ganancia Venta de Acciones418other
+ Resultados Positivos Extraordinarios42view
+ Recupero de Rezagos421other
+ Recupero de Deudores Incobrables422other
+ Ganancia Venta de Bienes de Uso423other
+ Donaciones obtenidas, ganandas, percibidas424other
+ Ganancia Venta Inversiones Permanentes425other
+ RESULTADOS NEGATIVOS5view
+ Resultados Negativos Ordinarios51view
+ Costo de Mercaderías Vendidas511view
+ Costo de Mercaderías Vendidas - Categoria de productos 01511.01other
+ Gastos en Depreciación de Bienes de Uso512other
+ Gastos en Amortización513other
+ Gastos en Sueldos y Jormales514other
+ Gastos en Cargas Sociales515other
+ Gastos en Impuestos516other
+ Gastos Bancarios517other
+ Gastos en Servicios Públicos518other
+ Gastos de Publicidad y Propaganda519other
+ Resultados Negativos Extraordinarios52view
+ Gastos en Siniestros521other
+ Donaciones Cedidas, Otorgadas522other
+ Pérdida Venta Bienes de Uso523other
+
+
+ Cuentas de Movimiento
+ .3.CC
+
+
+ view
+
+
+ Compras61view
+ Compras - Categoria de productos 0161.01other
+ Costos de Producción62other
+ Gastos de Administración63other
+ Gastos de Comercialización64other
+
+
+ Cuentas de Orden
+ .4.CO
+
+
+ view
+
+
+ CUENTAS DE ORDEN DEUDORAS71view
+ Mercaderias Recibidas en Consignación711other
+ Depósito de Valores Recibos en Garantía712other
+ Garantias Otorgadas713other
+ Documentos Descontados714other
+ Documentos Endosados715other
+ CUENTAS DE ORDEN ACREEDORAS72view
+ Comitente por Mercaderias Recibidas en Consignación721other
+ Acreedor por Garantías Otorgadas722other
+ Acreedor por Documentos Descontados723other
+
+
+ Argentina - Plan de Cuentas
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_ar/l10n_ar_wizard.xml b/addons/l10n_ar/l10n_ar_wizard.xml
new file mode 100644
index 00000000000..1180acb46e8
--- /dev/null
+++ b/addons/l10n_ar/l10n_ar_wizard.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Generate Chart of Accounts from a Chart Template
+ Generate Chart of Accounts from a Chart Template. You will be asked to pass the name of the company, the chart template to follow, the no. of digits to generate the code for your accounts and Bank account, currency to create Journals. Thus,the pure copy of chart Template is generated.
+ This is the same wizard that runs from Financial Management/Configuration/Financial Accounting/Financial Accounts/Generate Chart of Accounts from a Chart Template.
+
+
+ automatic
+
+
+
+
diff --git a/addons/l10n_at/__openerp__.py b/addons/l10n_at/__openerp__.py
index 0f70ec5ddbd..fc266e40d51 100644
--- a/addons/l10n_at/__openerp__.py
+++ b/addons/l10n_at/__openerp__.py
@@ -26,7 +26,10 @@
"website" : "http://www.conexus.at",
"category" : "Localization/Account Charts",
"depends" : ["account_chart", 'base_vat'],
- "description": "This module provides the standard Accounting Chart for Austria which is based on the Template from BMF.gv.at. Please keep in mind that you should review and adapt it with your Accountant, before using it in a live Environment.",
+ "description": """
+This module provides the standard Accounting Chart for Austria which is based on
+the Template from BMF.gv.at. Please keep in mind that you should review and adapt
+it with your Accountant, before using it in a live Environment.""",
"demo_xml" : [],
"update_xml" : ['account_tax_code.xml',"account_chart.xml",'account_tax.xml',"l10n_chart_at_wizard.xml"],
"auto_install": False,
diff --git a/addons/l10n_be/__openerp__.py b/addons/l10n_be/__openerp__.py
index d8bf8fb88a2..1f4bcfe6284 100644
--- a/addons/l10n_be/__openerp__.py
+++ b/addons/l10n_be/__openerp__.py
@@ -27,17 +27,23 @@ This is the base module to manage the accounting chart for Belgium in OpenERP.
After installing this module, the Configuration wizard for accounting is launched.
* We have the account templates which can be helpful to generate Charts of Accounts.
- * On that particular wizard, you will be asked to pass the name of the company, the chart template to follow, the no. of digits to generate, the code for your account and bank account, currency to create journals.
+ * On that particular wizard, you will be asked to pass the name of the company,
+ the chart template to follow, the no. of digits to generate, the code for your
+ account and bank account, currency to create journals.
Thus, the pure copy of Chart Template is generated.
Wizards provided by this module:
- * Partner VAT Intra: Enlist the partners with their related VAT and invoiced amounts.Prepares an XML file format.
- Path to access : Accounting/Reporting//Legal Statements/Belgium Statements/Partner VAT Listing
- * Periodical VAT Declaration: Prepares an XML file for Vat Declaration of the Main company of the User currently Logged in.
- Path to access : Accounting/Reporting/Legal Statements/Belgium Statements/Periodical VAT Declaration
- * Annual Listing Of VAT-Subjected Customers: Prepares an XML file for Vat Declaration of the Main company of the User currently Logged in.Based on Fiscal year
- Path to access : Accounting/Reporting/Legal Statements/Belgium Statements/Annual Listing Of VAT-Subjected Customers
+ * Partner VAT Intra: Enlist the partners with their related VAT and invoiced
+ amounts. Prepares an XML file format.
+ Path to access : Invoicing/Reporting/Legal Reports/Belgium Statements/Partner VAT Intra
+ * Periodical VAT Declaration: Prepares an XML file for Vat Declaration of
+ the Main company of the User currently Logged in.
+ Path to access : Invoicing/Reporting/Legal Reports/Belgium Statements/Periodical VAT Declaration
+ * Annual Listing Of VAT-Subjected Customers: Prepares an XML file for Vat
+ Declaration of the Main company of the User currently Logged in Based on
+ Fiscal year.
+ Path to access : Invoicing/Reporting/Legal Reports/Belgium Statements/Annual Listing Of VAT-Subjected Customers
""",
'author': 'Noviat & OpenERP SA',
diff --git a/addons/l10n_be/wizard/l10n_be_account_vat_declaration.py b/addons/l10n_be/wizard/l10n_be_account_vat_declaration.py
index eed2a537f62..95978ac52e7 100644
--- a/addons/l10n_be/wizard/l10n_be_account_vat_declaration.py
+++ b/addons/l10n_be/wizard/l10n_be_account_vat_declaration.py
@@ -45,7 +45,7 @@ class l10n_be_vat_declaration(osv.osv_memory):
'tax_code_id': fields.many2one('account.tax.code', 'Tax Code', domain=[('parent_id', '=', False)], required=True),
'msg': fields.text('File created', size=64, readonly=True),
'file_save': fields.binary('Save File'),
- 'ask_restitution': fields.boolean('Ask Restitution',help='It indicates whether a resitution is to made or not?'),
+ 'ask_restitution': fields.boolean('Ask Restitution',help='It indicates whether a restitution is to made or not?'),
'ask_payment': fields.boolean('Ask Payment',help='It indicates whether a payment is to made or not?'),
'client_nihil': fields.boolean('Last Declaration, no clients in client listing', help='Tick this case only if it concerns only the last statement on the civil or cessation of activity: ' \
'no clients to be included in the client listing.'),
diff --git a/addons/l10n_be/wizard/l10n_be_vat_intra.py b/addons/l10n_be/wizard/l10n_be_vat_intra.py
index 123d235f19b..2bfc4de6bad 100644
--- a/addons/l10n_be/wizard/l10n_be_vat_intra.py
+++ b/addons/l10n_be/wizard/l10n_be_vat_intra.py
@@ -60,7 +60,7 @@ class partner_vat_intra(osv.osv_memory):
'test_xml': fields.boolean('Test XML file', help="Sets the XML output as test file"),
'mand_id' : fields.char('Reference', size=14, help="Reference given by the Representative of the sending company."),
'msg': fields.text('File created', size=14, readonly=True),
- 'no_vat': fields.text('Partner With No VAT', size=14, readonly=True, help="The Partner whose VAT number is not defined they doesn't include in XML File."),
+ 'no_vat': fields.text('Partner With No VAT', size=14, readonly=True, help="The Partner whose VAT number is not defined and they are not included in XML File."),
'file_save' : fields.binary('Save File', readonly=True),
'country_ids': fields.many2many('res.country', 'vat_country_rel', 'vat_id', 'country_id', 'European Countries'),
'comments': fields.text('Comments'),
diff --git a/addons/l10n_be_hr_payroll/__openerp__.py b/addons/l10n_be_hr_payroll/__openerp__.py
index ebe2c8c0743..e0d7ada059b 100644
--- a/addons/l10n_be_hr_payroll/__openerp__.py
+++ b/addons/l10n_be_hr_payroll/__openerp__.py
@@ -25,14 +25,14 @@
'depends': ['hr_payroll'],
'version': '1.0',
'description': """
-Belgian Payroll Rules
-=====================
+Belgian Payroll Rules.
+======================
* Employee Details
* Employee Contracts
* Passport based Contract
- * Allowances / Deductions
- * Allow to configure Basic / Grows / Net Salary
+ * Allowances/Deductions
+ * Allow to configure Basic/Gross/Net Salary
* Employee Payslip
* Monthly Payroll Register
* Integrated with Holiday Management
diff --git a/addons/l10n_be_hr_payroll_account/__openerp__.py b/addons/l10n_be_hr_payroll_account/__openerp__.py
index ce6730df3da..81c7e243b39 100644
--- a/addons/l10n_be_hr_payroll_account/__openerp__.py
+++ b/addons/l10n_be_hr_payroll_account/__openerp__.py
@@ -25,7 +25,7 @@
'depends': ['l10n_be_hr_payroll', 'hr_payroll_account', 'l10n_be'],
'version': '1.0',
'description': """
-Accounting Data for Belgian Payroll Rules
+Accounting Data for Belgian Payroll Rules.
""",
'auto_install': True,
diff --git a/addons/l10n_be_invoice_bba/__openerp__.py b/addons/l10n_be_invoice_bba/__openerp__.py
index 9cdc97d0c71..9f4efe1b31e 100644
--- a/addons/l10n_be_invoice_bba/__openerp__.py
+++ b/addons/l10n_be_invoice_bba/__openerp__.py
@@ -32,7 +32,9 @@ Belgian localisation for in- and outgoing invoices (prereq to account_coda):
- Rename 'reference' field labels to 'Communication'
- Add support for Belgian Structured Communication
-A Structured Communication can be generated automatically on outgoing invoices according to the following algorithms:
+A Structured Communication can be generated automatically on outgoing invoices
+according to the following algorithms:
+
1) Random : +++RRR/RRRR/RRRDD+++
R..R = Random Digits, DD = Check Digits
2) Date : +++DOY/YEAR/SSSDD+++
@@ -40,8 +42,9 @@ A Structured Communication can be generated automatically on outgoing invoices a
3) Customer Reference +++RRR/RRRR/SSSDDD+++
R..R = Customer Reference without non-numeric characters, SSS = Sequence Number, DD = Check Digits)
-The preferred type of Structured Communication and associated Algorithm can be specified on the Partner records.
-A 'random' Structured Communication will generated if no algorithm is specified on the Partner record.
+The preferred type of Structured Communication and associated Algorithm can be
+specified on the Partner records. A 'random' Structured Communication will
+generated if no algorithm is specified on the Partner record.
""",
'depends': ['account'],
diff --git a/addons/l10n_be_invoice_bba/account_invoice_view.xml b/addons/l10n_be_invoice_bba/account_invoice_view.xml
index 51c39a4b3be..bf1cf96f537 100644
--- a/addons/l10n_be_invoice_bba/account_invoice_view.xml
+++ b/addons/l10n_be_invoice_bba/account_invoice_view.xml
@@ -12,7 +12,7 @@
+ on_change="generate_bbacomm(type,reference_type, partner_id,reference, context)" colspan="1"/>
diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py
index 4cdbca84c0a..bb03e4d6d02 100644
--- a/addons/l10n_be_invoice_bba/invoice.py
+++ b/addons/l10n_be_invoice_bba/invoice.py
@@ -44,7 +44,7 @@ class account_invoice(osv.osv):
#l_logger.warning('reference_type = %s' %res )
return res
- def check_bbacomm(self, val):
+ def check_bbacomm(self, val):
supported_chars = '0-9+*/ '
pattern = re.compile('[^' + supported_chars + ']')
if pattern.findall(val or ''):
@@ -75,10 +75,7 @@ class account_invoice(osv.osv):
if (type == 'out_invoice'):
reference_type = self.pool.get('res.partner').browse(cr, uid, partner_id).out_inv_comm_type
if reference_type:
- algorithm = self.pool.get('res.partner').browse(cr, uid, partner_id).out_inv_comm_algorithm
- if not algorithm:
- algorithm = 'random'
- reference = self.generate_bbacomm(cr, uid, ids, type, reference_type, algorithm, partner_id, '')['value']['reference']
+ reference = self.generate_bbacomm(cr, uid, ids, type, reference_type, partner_id, '', context={})['value']['reference']
res_update = {
'reference_type': reference_type or 'none',
'reference': reference,
@@ -86,17 +83,15 @@ class account_invoice(osv.osv):
result['value'].update(res_update)
return result
- def generate_bbacomm(self, cr, uid, ids, type, reference_type, algorithm, partner_id, reference):
+ def generate_bbacomm(self, cr, uid, ids, type, reference_type, partner_id, reference, context=None):
partner_obj = self.pool.get('res.partner')
- reference = reference or ''
+ reference = reference or ''
+ algorithm = False
+ if partner_id:
+ algorithm = partner_obj.browse(cr, uid, partner_id, context=context).out_inv_comm_algorithm
+ algorithm = algorithm or 'random'
if (type == 'out_invoice'):
if reference_type == 'bba':
- if not algorithm:
- if partner_id:
- algorithm = partner_obj.browse(cr, uid, partner_id).out_inv_comm_algorithm
- if not algorithm:
- if not algorithm:
- algorithm = 'random'
if algorithm == 'date':
if not self.check_bbacomm(reference):
doy = time.strftime('%j')
diff --git a/addons/l10n_be_invoice_bba/partner_view.xml b/addons/l10n_be_invoice_bba/partner_view.xml
index 8be0f31d612..d78df2d7a42 100644
--- a/addons/l10n_be_invoice_bba/partner_view.xml
+++ b/addons/l10n_be_invoice_bba/partner_view.xml
@@ -9,7 +9,7 @@
form
-
+
diff --git a/addons/l10n_br/__openerp__.py b/addons/l10n_br/__openerp__.py
index 909d4f53600..022fa1b6a4d 100644
--- a/addons/l10n_br/__openerp__.py
+++ b/addons/l10n_br/__openerp__.py
@@ -22,8 +22,8 @@
'name': 'Brazilian - Accounting',
'category': 'Localization/Account Charts',
'description': """
-Base module for the Brazilian localization
-==========================================
+Base module for the Brazilian localization.
+===========================================
This module consists in:
@@ -41,9 +41,23 @@ This module consists in:
- Tax Situation Code (CST) required for the electronic fiscal invoicing (NFe)
-The field tax_discount has also been added in the account.tax.template and account.tax objects to allow the proper computation of some Brazilian VATs such as ICMS. The chart of account creation wizard has been extended to propagate those new data properly.
+The field tax_discount has also been added in the account.tax.template and account.tax
+objects to allow the proper computation of some Brazilian VATs such as ICMS. The
+chart of account creation wizard has been extended to propagate those new data properly.
-It's important to note however that this module lack many implementations to use OpenERP properly in Brazil. Those implementations (such as the electronic fiscal Invoicing which is already operational) are brought by more than 15 additional modules of the Brazilian Launchpad localization project https://launchpad.net/openerp.pt-br-localiz and their dependencies in the extra addons branch. Those modules aim at not breaking with the remarkable OpenERP modularity, this is why they are numerous but small. One of the reasons for maintaining those modules apart is that Brazilian Localization leaders need commit rights agility to complete the localization as companies fund the remaining legal requirements (such as soon fiscal ledgers, accounting SPED, fiscal SPED and PAF ECF that are still missing as September 2011). Those modules are also strictly licensed under AGPL V3 and today don't come with any additional paid permission for online use of 'private modules'.""",
+It's important to note however that this module lack many implementations to use
+OpenERP properly in Brazil. Those implementations (such as the electronic fiscal
+Invoicing which is already operational) are brought by more than 15 additional
+modules of the Brazilian Launchpad localization project
+https://launchpad.net/openerp.pt-br-localiz and their dependencies in the extra
+addons branch. Those modules aim at not breaking with the remarkable OpenERP
+modularity, this is why they are numerous but small. One of the reasons for
+maintaining those modules apart is that Brazilian Localization leaders need commit
+rights agility to complete the localization as companies fund the remaining legal
+requirements (such as soon fiscal ledgers, accounting SPED, fiscal SPED and PAF
+ECF that are still missing as September 2011). Those modules are also strictly
+licensed under AGPL V3 and today don't come with any additional paid permission
+for online use of 'private modules'.""",
'license': 'AGPL-3',
'author': 'Akretion, OpenERP Brasil',
'website': 'http://openerpbrasil.org',
diff --git a/addons/l10n_ch/__openerp__.py b/addons/l10n_ch/__openerp__.py
index ab56327573c..d3b7e796aee 100644
--- a/addons/l10n_ch/__openerp__.py
+++ b/addons/l10n_ch/__openerp__.py
@@ -23,8 +23,8 @@
"description" : """
Swiss localisation :
- DTA generation for a lot of payment types
- - BVR management (number generation, report, etc..)
- - Import account move from the bank file (like v11 etc..)
+ - BVR management (number generation, report.)
+ - Import account move from the bank file (like v11)
- Simplify the way you handle the bank statement for reconciliation
You can also add ZIP and bank completion with:
@@ -36,10 +36,11 @@ You can also add ZIP and bank completion with:
------------------------------------------------------------------------
-Module incluant la localisation Suisse de TinyERP revu et corrigé par Camptocamp. Cette nouvelle version
-comprend la gestion et l'émissionde BVR, le paiement électronique via DTA (pour les banques, le système postal est en développement)
-et l'import du relevé de compte depuis la banque de manière automatisée.
-De plus, nous avons intégré la définition de toutes les banques Suisses(adresse, swift et clearing).
+Module incluant la localisation Suisse de TinyERP revu et corrigé par Camptocamp.
+Cette nouvelle version comprend la gestion et l'émissionde BVR, le paiement
+électronique via DTA (pour les banques, le système postal est en développement)
+et l'import du relevé de compte depuis la banque de manière automatisée. De plus,
+nous avons intégré la définition de toutes les banques Suisses(adresse, swift et clearing).
Par ailleurs, conjointement à ce module, nous proposons la complétion NPA:
@@ -52,12 +53,11 @@ Vous pouvez ajouter la completion des banques et des NPA avec with:
--------------------------------------------------------------------------
TODO :
-- Implement bvr import partial reconciliation
-- Replace wizard by osv_memory when possible
-- Add mising HELP
-- Finish code comment
-- Improve demo data
-
+ - Implement bvr import partial reconciliation
+ - Replace wizard by osv_memory when possible
+ - Add mising HELP
+ - Finish code comment
+ - Improve demo data
""",
"version": "6.1",
diff --git a/addons/l10n_ch/company_view.xml b/addons/l10n_ch/company_view.xml
index 0d107f63723..108982526b6 100644
--- a/addons/l10n_ch/company_view.xml
+++ b/addons/l10n_ch/company_view.xml
@@ -7,22 +7,22 @@
form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/addons/l10n_ch/wizard/bvr_import_view.xml b/addons/l10n_ch/wizard/bvr_import_view.xml
index 222aad3b6b9..18ee44efcf9 100644
--- a/addons/l10n_ch/wizard/bvr_import_view.xml
+++ b/addons/l10n_ch/wizard/bvr_import_view.xml
@@ -30,15 +30,15 @@
new
-
+ account.bank.statement.form.inheritaccount.bank.statementform
-
-
-
+
+
+
diff --git a/addons/l10n_cl/__init__.py b/addons/l10n_cl/__init__.py
new file mode 100644
index 00000000000..92da5dbf966
--- /dev/null
+++ b/addons/l10n_cl/__init__.py
@@ -0,0 +1,32 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (c) 2011 Cubic ERP - Teradata SAC. (http://cubicerp.com).
+#
+# WARNING: This program as such is intended to be used by professional
+# programmers who take the whole responsability of assessing all potential
+# consequences resulting from its eventual inadequacies and bugs
+# End users who are looking for a ready-to-use solution with commercial
+# garantees and support are strongly adviced to contract a Free Software
+# Service Company
+#
+# 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 2
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+##############################################################################
+
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/pad_project/models/__init__.py b/addons/l10n_cl/__openerp__.py
similarity index 54%
rename from addons/pad_project/models/__init__.py
rename to addons/l10n_cl/__openerp__.py
index 098de31a5a4..8c1e1e45b50 100644
--- a/addons/pad_project/models/__init__.py
+++ b/addons/l10n_cl/__openerp__.py
@@ -1,8 +1,8 @@
-# -*- coding: utf-8 -*-
+# -*- encoding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
+# Copyright (C) 2011 Cubic ERP - Teradata SAC ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -17,8 +17,35 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
#
-##############################################################################
+##############################################################################
+{
+ "name": "Chile Localization Chart Account",
+ "version": "1.0",
+ "description": """
+Chilean accounting chart and tax localization.
-import project_task
+Plan contable chileno e impuestos de acuerdo a disposiciones vigentes
+ """,
+ "author": "Cubic ERP",
+ "website": "http://cubicERP.com",
+ "category": "Localization/Account Charts",
+ "depends": [
+ "account_chart",
+ ],
+ "data":[
+ "account_tax_code.xml",
+ "l10n_cl_chart.xml",
+ "account_tax.xml",
+ "l10n_cl_wizard.xml",
+ ],
+ "demo_xml": [
+ ],
+ "update_xml": [
+ ],
+ "active": False,
+ "installable": True,
+ "certificate" : "",
+ 'images': ['images/config_chart_l10n_cl.jpeg','images/l10n_cl_chart.jpeg'],
+}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_cl/account_tax.xml b/addons/l10n_cl/account_tax.xml
new file mode 100644
index 00000000000..1db0d2637f4
--- /dev/null
+++ b/addons/l10n_cl/account_tax.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ IVA 19% Venta
+ 0.190000
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+
+ IVA 19% Compra
+ 0.190000
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_cl/account_tax_code.xml b/addons/l10n_cl/account_tax_code.xml
new file mode 100644
index 00000000000..95c91b60d78
--- /dev/null
+++ b/addons/l10n_cl/account_tax_code.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+ Chile Impuestos
+
+
+ Base Imponible
+
+
+
+ Base Imponible - Ventas
+
+
+
+ Ventas Gravadas con IVA
+
+
+
+ Ventas NO Gravadas (Exoneradas)
+
+
+
+ Ventas Gravadas Fuera de Ámbito
+
+
+
+ Base Imponible - Compras
+
+
+
+ Compras Gravadas con IVA
+
+
+
+ Compras NO Gravadas (Exoneradas)
+
+
+
+ Compras Gravadas Fuera de Ámbito
+
+
+
+
+ Impuesto General a las Ventas (IVA) Total a Pagar
+
+
+
+ Impuesto Pagado
+
+
+
+ Impuesto Pagado IVA
+
+ -1
+
+
+ Impuesto Pagado de Exonerados al IVA
+
+ -1
+
+
+ Impuesto Pagado Fuera de Ámbito
+
+ -1
+
+
+ Impuesto Cobrado
+
+
+
+ Impuesto Cobrado IVA
+
+
+
+ Impuesto Cobrado de Exonerados al IVA
+
+
+
+ Impuesto Cobrado Fuera de Ámbito
+
+
+
+
+ Impuestos Directos a Pagar
+
+
+
+ Impuesto a la Renta Primera Categoría a Pagar
+
+
+
+
diff --git a/addons/l10n_cl/i18n/es.po b/addons/l10n_cl/i18n/es.po
new file mode 100644
index 00000000000..f8b17197008
--- /dev/null
+++ b/addons/l10n_cl/i18n/es.po
@@ -0,0 +1,72 @@
+# Spanish translation for openobject-addons
+# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2011-01-11 11:15+0000\n"
+"PO-Revision-Date: 2011-01-10 13:46+0000\n"
+"Last-Translator: Yury Tello \n"
+"Language-Team: Spanish \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2011-01-15 05:56+0000\n"
+"X-Generator: Launchpad (build 12177)\n"
+
+#. module: l10n_cl
+#: model:ir.module.module,description:l10n_cl.module_meta_information
+msgid ""
+"\n"
+" Chilean Accounting : chart of Account\n"
+" "
+msgstr ""
+"\n"
+" Contabilidad Peruana : Plan de cuentas\n"
+" "
+
+#. module: l10n_cl
+#: model:ir.module.module,shortdesc:l10n_cl.module_meta_information
+msgid "Chilean Chart of Account"
+msgstr "Plan de cuentas de Chile"
+
+#. module: l10n_cl
+#: model:ir.actions.todo,note:l10n_cl.config_call_account_template_in_minimal
+msgid ""
+"Generate Chart of Accounts from a Chart Template. You will be asked to pass "
+"the name of the company, the chart template to follow, the no. of digits to "
+"generate the code for your accounts and Bank account, currency to create "
+"Journals. Thus,the pure copy of chart Template is generated.\n"
+"\tThis is the same wizard that runs from Financial "
+"Management/Configuration/Financial Accounting/Financial Accounts/Generate "
+"Chart of Accounts from a Chart Template."
+msgstr ""
+"Generar el plan contable a partir de una plantilla de plan contable. Se le "
+"pedirá el nombre de la compañia, la plantilla de plan contable a utilizar, "
+"el número de dígitos para generar el código de las cuentas y de la cuenta "
+"bancaria, la moneda para crear los diarios. Así pues, se genere una copia "
+"exacta de la plantilla de plan contable.\n"
+"\tEste es el mismo asistente que se ejecuta desde Contabilidad y finanzas / "
+"Configuración / Contabilidad financiera / Cuentas financieras / Generar el "
+"plan contable a partir de una plantilla de plan contable."
+
+#~ msgid "Liability"
+#~ msgstr "Pasivo"
+
+#~ msgid "Asset"
+#~ msgstr "Activo"
+
+#~ msgid "Closed"
+#~ msgstr "Cerrado"
+
+#~ msgid "Income"
+#~ msgstr "Ingreso"
+
+#~ msgid "Expense"
+#~ msgstr "Gasto"
+
+#~ msgid "View"
+#~ msgstr "Vista"
diff --git a/addons/l10n_cl/i18n/es_CL.po b/addons/l10n_cl/i18n/es_CL.po
new file mode 100644
index 00000000000..cd0a9f9c43e
--- /dev/null
+++ b/addons/l10n_cl/i18n/es_CL.po
@@ -0,0 +1,53 @@
+# Spanish (Paraguay) translation for openobject-addons
+# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2011.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2011-01-11 11:15+0000\n"
+"PO-Revision-Date: 2011-03-21 16:23+0000\n"
+"Last-Translator: FULL NAME \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2011-03-22 04:36+0000\n"
+"X-Generator: Launchpad (build 12559)\n"
+
+#. module: l10n_cl
+#: model:ir.module.module,description:l10n_cl.module_meta_information
+msgid ""
+"\n"
+" Chilean Accounting : chart of Account\n"
+" "
+msgstr ""
+"\n"
+" Contabilidad Chile : Plan de cuentas\n"
+" "
+
+#. module: l10n_cl
+#: model:ir.module.module,shortdesc:l10n_cl.module_meta_information
+msgid "Chilean Chart of Account"
+msgstr "Plan de cuentas de la Chile"
+
+#. module: l10n_cl
+#: model:ir.actions.todo,note:l10n_cl.config_call_account_template_in_minimal
+msgid ""
+"Generate Chart of Accounts from a Chart Template. You will be asked to pass "
+"the name of the company, the chart template to follow, the no. of digits to "
+"generate the code for your accounts and Bank account, currency to create "
+"Journals. Thus,the pure copy of chart Template is generated.\n"
+"\tThis is the same wizard that runs from Financial "
+"Management/Configuration/Financial Accounting/Financial Accounts/Generate "
+"Chart of Accounts from a Chart Template."
+msgstr ""
+"Generar el plan contable a partir de una plantilla de plan contable. Se le "
+"pedirá el nombre de la compañía, la plantilla de plan contable a utilizar, "
+"el número de dígitos para generar el código de las cuentas y de la cuenta "
+"bancaria, la moneda para crear los diarios. Así pues, se genere una copia "
+"exacta de la plantilla de plan contable.\n"
+"\tEste es el mismo asistente que se ejecuta desde Contabilidad y finanzas / "
+"Configuración / Contabilidad financiera / Cuentas financieras / Generar el "
+"plan contable a partir de una plantilla de plan contable."
diff --git a/addons/l10n_cl/i18n/l10n_cl.pot b/addons/l10n_cl/i18n/l10n_cl.pot
new file mode 100644
index 00000000000..9b3704b75e1
--- /dev/null
+++ b/addons/l10n_cl/i18n/l10n_cl.pot
@@ -0,0 +1,35 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * l10n_cl
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 6.1.0-rc1\n"
+"Report-Msgid-Bugs-To: soporte@cubicerp.com\n"
+"POT-Creation-Date: 2011-01-11 11:15:31+0000\n"
+"PO-Revision-Date: 2011-01-11 11:15:31+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: l10n_cl
+#: model:ir.module.module,description:l10n_cl.module_meta_information
+msgid "\n"
+" Chilean Accounting : chart of Account\n"
+" "
+msgstr ""
+
+#. module: l10n_cl
+#: model:ir.module.module,shortdesc:l10n_cl.module_meta_information
+msgid "Chilean Chart of Account"
+msgstr ""
+
+#. module: l10n_cl
+#: model:ir.actions.todo,note:l10n_cl.config_call_account_template_in_minimal
+msgid "Generate Chart of Accounts from a Chart Template. You will be asked to pass the name of the company, the chart template to follow, the no. of digits to generate the code for your accounts and Bank account, currency to create Journals. Thus,the pure copy of chart Template is generated.\n"
+" This is the same wizard that runs from Financial Management/Configuration/Financial Accounting/Financial Accounts/Generate Chart of Accounts from a Chart Template."
+msgstr ""
+
diff --git a/addons/l10n_cl/l10n_cl_chart.xml b/addons/l10n_cl/l10n_cl_chart.xml
new file mode 100644
index 00000000000..8bc4272c2eb
--- /dev/null
+++ b/addons/l10n_cl/l10n_cl_chart.xml
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+ noneviewVistanone
+
+ balanceBG_ACC_10Efectivo y Equivalentes al Efectivoasset
+ detailBG_ACC_20Otros Activos Financieros Corrientesasset
+ unreconciledBG_ACC_30Deudores Comercialesasset
+ unreconciledBG_ACC_50Otras Cuentas por Cobrarasset
+ balanceBG_ACC_60Inventariosasset
+
+ detailBG_ACN_10Derechos por Cobrar No Corrienteasset
+ detailBG_ACN_40Otros Activos No Financierosasset
+ balanceBG_ACN_50Propiedades, Planta y Equipoasset
+
+ unreconciledBG_PAC_10Otros Pasivos Financierosliability
+ unreconciledBG_PAC_20Cuentas por Pagar Comercialesliability
+ unreconciledBG_PAC_35Otras Cuentas por Pagarliability
+ unreconciledBG_PAC_40Otras Provisiones Corrientesliability
+ unreconciledBG_PAC_45Pasivos por Impuestos Corrientesliability
+
+ unreconciledBG_PAN_10Otros Pasivos Financieros No Corrientesliability
+ unreconciledBG_PAN_20Otros Cuentas por Pagar No Corrientesliability
+ unreconciledBG_PAN_40Otras Provisiones No Corrientesliability
+
+ balanceBG_PTN_10Patrimonio Netoliability
+
+ noneEGP_FU_010Ingresos por Actividades Ordinariasincome
+ noneEGP_FU_030Costo de Ventasexpense
+ noneEGP_FU_040Gastos de Administraciónexpense
+ noneEGP_FU_050Costos por Distribuciónexpense
+ unreconciledEGP_FU_060Ingresos Financierosincome
+ noneEGP_FU_070Costos Financierosexpense
+ noneEGP_FU_080Otros Ingresosincome
+ noneEGP_FU_090Otros Gastosexpense
+ noneEGP_FU_120Gasto Impuesto a las Rentaexpense
+ noneEGP_FU_160Ganancia (Pérdida)income
+
+ noneEGP_NA_010Compras de Activo Fijonone
+
+ noneORDCuentas de Ordennone
+
+ noneNCLASIFICADOCuentas No Clasificadasnone
+
+
+
+
+ Chile
+ pcge
+
+ view
+
+
+ inventario del Balance General
+ .1.BG
+
+
+ view
+
+
+ ACTIVOS1view
+ Activo Circulante11view
+ Activo Circulante - Caja111view
+ Activo Circulante - Caja / efectivo CLP111.001liquidity
+ Activo Circulante - Moneda Extranjera112view
+ Activo Circulante - Caja / efectivo USD112.001liquidity
+ Activo Circulante - Fondos fijos113view
+ Activo Circulante - Fondos fijos / caja chica 01 CLP113.001liquidity
+ Activo Circulante - Bancos114view
+ Activo Circulante.../ BCO. CTA CTE CLP114.001liquidity
+ Activo Circulante - Valores a Depositar 115other
+ Activo Circulante - Recaudaciones a Depositar 116other
+ Documentos por Cobrar12view
+ Documentos por Cobrar / Deudores por Ventas121receivable1
+ Documentos por Cobrar / Deudores Morosos122receivable1
+ Documentos por Cobrar / Deudores en Gestión Judicial123receivable
+ Documentos por Cobrar / Deudores Varios124receivable
+ Documentos por Cobrar / (-) Previsión para Incobrables125receivable1
+ Cuentas por Cobrar13view
+ Cuentas por Cobrar / Préstamos otorgados131receivable
+ Cuentas por Cobrar / Anticipos a Proveedores132receivable
+ Cuentas por Cobrar / Anticipo de Impuestos133receivable
+ Cuentas por Cobrar / Anticipo al Personal134receivable
+ Cuentas por Cobrar / Alquileres Pagados por Adelantado135receivable
+ Cuentas por Cobrar / Intereses Pagados por Adelantado136receivable
+ Cuentas por Cobrar / Accionistas137receivable
+ Cuentas por Cobrar / (-) Previsión para Descuentos138receivable
+ Cuentas por Cobrar / (-) Intereses (+) a Devengar139receivable
+ Inversiones Financieras14view
+ Inversiones / Acciones Transitorias141other
+ Inversiones / Acciones Permanentes142other
+ Inversiones / Títulos Públicos143other
+ Inversiones / (-) Previsión para Devalorización de Acciones144other
+ Existencias15view
+ Existencias - Mercaderías151view
+ Existencias - Mercaderías / Categoria de productos 01151.01other
+ Existencias - Mercaderías en Tránsito152other
+ Materias primas153other
+ Productos en Curso de Elaboración154other
+ Productos Elaborados155other
+ Materiales Varios 156other
+ (-) Previsión para Desvalorización de Existencias157other
+ Activo Fijo16view
+ Activo Fijo / Inmuebles161other
+ Activo Fijo / Maquinaria162other
+ Activo Fijo / Equipos163other
+ Activo Fijo / Material Rodante Motorizado164other
+ Activo Fijo / (-) Depreciación Acumulada165other
+ Activo Intangible17view
+ Activo Intangible / Derecho de Llaves171other
+ Activo Intangible / Concesiones y Franquicias172other
+ Activo Intangible / Marcas y Patentes de Invención173other
+ Activo Intangible / (-) Amortización Acumulada174other
+ PASIVOS2view
+ Cuentas por Pagar21view
+ Cuentas por Pagar / Proveedores211payable1
+ Cuentas por Pagar / Anticipos de Clientes212payable1
+ Cuentas por Pagar / (-) Intereses a Devengar por Compras al Crédito213payable1
+ Pasivo Circulante22view
+ Pasivo Circulante / Adelantos en Cuenta Corriente221payable
+ Pasivo Circulante / Prestamos222payable
+ Pasivo Circulante / Obligaciones a Pagar223payable
+ Pasivo Circulante / Intereses a Pagar224payable
+ Pasivo Circulante / Debentures Emitidos225payable
+ Impuestos por Pagar23view
+ Impuestos por Pagar / IVA a Pagar231other
+ Impuestos por Pagar / Impuesto a la Renta a Pagar232other
+ Remuneraciones por Pagar24view
+ Remuneraciones por Pagar / Sueldos a Pagar241payable
+ Remuneraciones por Pagar / Cargas Sociales a Pagar242payable
+ Remuneraciones por Pagar / Provisión para Sueldo Anual Complementario243payable
+ Remuneraciones por Pagar / Retenciones a Depositar244payable
+ Otras Cuentas por Pagar25view
+ Otras Cuentas por Pagar / Acreedores Varios251payable
+ Otras Cuentas por Pagar / Dividendos a Pagar252payable
+ Otras Cuentas por Pagar / Cobros por Adelantado253payable
+ Otras Cuentas por Pagar / Honorarios Directores y Síndicos a Pagar254payable
+ Provisiones26view
+ Provisiones / Previsión Indemnización por Despidos261payable
+ Provisiones / Previsión para juicios Pendientes262payable
+ Provisiones / Previsión para Garantías por Service263payable
+ PATRIMONIO3view
+ Capital31view
+ Capital / Capital Propio311other
+ Capital / Acciones en Circulación312other
+ Capital / Dividendos a Distribuir en Acciones313other
+ Capital / (-) Descuento de Emisión de Acciones314other
+ Aportes No Capitalizados32view
+ Aportes No Capitalizados / Primas de Emsión321other
+ Aportes No Capitalizados / Aportes Irrevocables Futura Suscripción de Acciones322other
+ Ajustes al Patrimonio33view
+ Ajustes al Patrimonio / Revaluo Técnico de Activo Fijo331other
+ Futuras Eventualidades34view
+ Reserva Legal341other
+ Reserva Estatutaria342other
+ Reserva Facultativa343other
+ Reserva para Renovación de Activo Fijo344other
+ Resultados No Asignados35view
+ Resultados Acumulados351other
+ Resultados Acumulados del Ejercicio Anterior352other
+ Utilidades y Pérdidas del Ejercicio353other
+ Resultado del Ejercicio354other
+
+
+ Cuentas de Resultado
+ .2.GP
+
+
+ view
+
+
+ RESULTADO GANANCIA4view
+ Ingresos de Explotación41view
+ Ventas411view
+ Ventas - Categoria de productos 01411.01other
+ Intereses gananados, obtenidos, percibidos412other
+ Alquileres gananados, obtenidos, percibidos413other
+ Comisiones gananados, obtenidos, percibidos414other
+ Descuentos gananados, obtenidos, percibidos415other
+ Interese sobre Inversiones416other
+ Honorarios gananados, obtenidos, percibidos417other
+ Ganancia Venta de Acciones418other
+ Ingresos Fuera de Explotación42view
+ Recupero de Rezagos421other
+ Recupero de Deudores Incobrables422other
+ Ganancia Venta de Activo Fijo423other
+ Donaciones obtenidas, ganandas, percibidas424other
+ Ganancia Venta Inversiones Permanentes425other
+ RESULTADO PÉRDIDA5view
+ Egresos de Explotación51view
+ Costo de Mercaderías Vendidas511view
+ Costo de Mercaderías Vendidas - Categoria de productos 01511.01other
+ Gastos en Depreciación de Activo Fijo512other
+ Gastos en Amortización513other
+ Gastos en Sueldos y Jornales514other
+ Gastos en Cargas Sociales515other
+ Gastos en Impuestos516other
+ Gastos Bancarios517other
+ Gastos en Servicios Públicos518other
+ Gastos de Publicidad y Propaganda519other
+ Egresos Fuera de Explotación52view
+ Gastos en Siniestros521other
+ Donaciones Cedidas, Otorgadas522other
+ Pérdida Venta Activo Fijo523other
+
+
+ Cuentas de Movimiento
+ .3.CC
+
+
+ view
+
+
+ Compras61view
+ Compras - Categoria de productos 0161.01other
+ Costos de Producción62other
+ Gastos de Administración63other
+ Gastos de Comercialización64other
+
+
+ Cuentas de Orden
+ .4.CO
+
+
+ view
+
+
+ CUENTAS DE ORDEN DEUDORAS71view
+ Mercaderias Recibidas en Consignación711other
+ Depósito de Valores Recibos en Garantía712other
+ Garantias Otorgadas713other
+ Documentos Descontados714other
+ Documentos Endosados715other
+ CUENTAS DE ORDEN ACREEDORAS72view
+ Comitente por Mercaderias Recibidas en Consignación721other
+ Acreedor por Garantías Otorgadas722other
+ Acreedor por Documentos Descontados723other
+
+
+ Chile - Plan de Cuentas
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_cl/l10n_cl_wizard.xml b/addons/l10n_cl/l10n_cl_wizard.xml
new file mode 100644
index 00000000000..191230c5010
--- /dev/null
+++ b/addons/l10n_cl/l10n_cl_wizard.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Generate Chart of Accounts from a Chart Template
+ Generate Chart of Accounts from a Chart Template. You will be asked to pass the name of the company, the chart template to follow, the no. of digits to generate the code for your accounts and Bank account, currency to create Journals. Thus,the pure copy of chart Template is generated.
+ This is the same wizard that runs from Financial Management/Configuration/Financial Accounting/Financial Accounts/Generate Chart of Accounts from a Chart Template.
+
+
+ automatic
+
+
+
+
diff --git a/addons/l10n_cn/__openerp__.py b/addons/l10n_cn/__openerp__.py
index 6e66c77e45b..e173c5ebba2 100644
--- a/addons/l10n_cn/__openerp__.py
+++ b/addons/l10n_cn/__openerp__.py
@@ -26,9 +26,9 @@
"website":"http://openerp-china.org",
"url":"http://code.google.com/p/openerp-china/source/browse/#svn/trunk/l10n_cn",
"description": """
- 添加中文省份数据
- 科目类型\会计科目表模板\增值税\辅助核算类别\管理会计凭证簿\财务会计凭证簿
- ============================================================
+添加中文省份数据
+科目类型\会计科目表模板\增值税\辅助核算类别\管理会计凭证簿\财务会计凭证簿
+============================================================
""",
"depends" : ["base","account"],
'init_xml': [
diff --git a/addons/l10n_cr/__openerp__.py b/addons/l10n_cr/__openerp__.py
index e224f22358a..562af835398 100644
--- a/addons/l10n_cr/__openerp__.py
+++ b/addons/l10n_cr/__openerp__.py
@@ -44,14 +44,14 @@ Chart of accounts for Costa Rica.
=================================
Includes:
-* account.type
-* account.account.template
-* account.tax.template
-* account.tax.code.template
-* account.chart.template
+ * account.type
+ * account.account.template
+ * account.tax.template
+ * account.tax.code.template
+ * account.chart.template
-Everything is in English with Spanish translation. Further translations are welcome, please go to
-http://translations.launchpad.net/openerp-costa-rica
+Everything is in English with Spanish translation. Further translations are welcome,
+please go to http://translations.launchpad.net/openerp-costa-rica.
""",
'depends': ['account', 'account_chart', 'base'],
'init_xml': [],
diff --git a/addons/l10n_es/__openerp__.py b/addons/l10n_es/__openerp__.py
index 95b8ab7b57c..b2897e07748 100644
--- a/addons/l10n_es/__openerp__.py
+++ b/addons/l10n_es/__openerp__.py
@@ -31,14 +31,14 @@
Spanish Charts of Accounts (PGCE 2008).
=======================================
-* Defines the following chart of account templates:
- * Spanish General Chart of Accounts 2008.
- * Spanish General Chart of Accounts 2008 for small and medium companies.
-* Defines templates for sale and purchase VAT.
-* Defines tax code templates.
+ * Defines the following chart of account templates:
+ * Spanish General Chart of Accounts 2008
+ * Spanish General Chart of Accounts 2008 for small and medium companies
+ * Defines templates for sale and purchase VAT
+ * Defines tax code templates
-Note: You should install the l10n_ES_account_balance_report module
-for yearly account reporting (balance, profit & losses).
+Note: You should install the l10n_ES_account_balance_report module for yearly
+ account reporting (balance, profit & losses).
""",
"license" : "GPL-3",
"depends" : ["account", "base_vat", "base_iban"],
diff --git a/addons/l10n_fr/__openerp__.py b/addons/l10n_fr/__openerp__.py
index 99849d62649..ec31f71513a 100644
--- a/addons/l10n_fr/__openerp__.py
+++ b/addons/l10n_fr/__openerp__.py
@@ -35,11 +35,21 @@
This is the module to manage the accounting chart for France in OpenERP.
========================================================================
-This module applies to companies based in France mainland. It doesn't apply to companies based in the DOM-TOMs (Guadeloupe, Martinique, Guyane, Réunion, Mayotte, etc...)
+This module applies to companies based in France mainland. It doesn't apply to
+companies based in the DOM-TOMs (Guadeloupe, Martinique, Guyane, Réunion, Mayotte).
-This localisation module creates the VAT taxes of type "tax included" for purchases (it is notably required when you use the module "hr_expense"). Beware that these "tax included" VAT taxes are not managed by the fiscal positions provided by this module (because it is complex to manage both "tax excluded" and "tax included" scenarios in fiscal positions).
+This localisation module creates the VAT taxes of type "tax included" for purchases
+(it is notably required when you use the module "hr_expense"). Beware that these
+"tax included" VAT taxes are not managed by the fiscal positions provided by this
+module (because it is complex to manage both "tax excluded" and "tax included"
+scenarios in fiscal positions).
-This localisation module doesn't properly handle the scenario when a France-mainland company sells services to a company based in the DOMs. We could manage it in the fiscal positions, but it would require to differentiate between "product" VAT taxes and "service" VAT taxes. We consider that it is too "heavy" to have this by default in l10n_fr ; companies that sell services to DOM-based companies should update the configuration of their taxes and fiscal positions manually.
+This localisation module doesn't properly handle the scenario when a France-mainland
+company sells services to a company based in the DOMs. We could manage it in the
+fiscal positions, but it would require to differentiate between "product" VAT taxes
+and "service" VAT taxes. We consider that it is too "heavy" to have this by default
+in l10n_fr; companies that sell services to DOM-based companies should update the
+configuration of their taxes and fiscal positions manually.
Credits: Sistheo, Zeekom, CrysaLEAD, Akretion and Camptocamp.
""",
diff --git a/addons/l10n_fr_hr_payroll/__openerp__.py b/addons/l10n_fr_hr_payroll/__openerp__.py
index b9bc3b74f22..c3b2c332a46 100755
--- a/addons/l10n_fr_hr_payroll/__openerp__.py
+++ b/addons/l10n_fr_hr_payroll/__openerp__.py
@@ -25,19 +25,22 @@
'depends': ['hr_payroll', 'l10n_fr'],
'version': '1.0',
'description': """
-French Payroll Rules
-=======================
+French Payroll Rules.
+=====================
- -Configuration of hr_payroll for french localization
- -All main contributions rules for french payslip, for 'cadre' and 'non-cadre'
- -New payslip report
+ - Configuration of hr_payroll for French localization
+ - All main contributions rules for French payslip, for 'cadre' and 'non-cadre'
+ - New payslip report
- TODO :
- -Integration with holidays module for deduction and allowance
- -Integration with hr_payroll_account for the automatic account_move_line creation from the payslip
- -Continue to integrate the contribution. Only the main contribution are currently implemented
- -Remake the report under webkit
- -The payslip.line with appears_in_payslip = False should appears in the payslip interface, but not in the payslip report
+TODO :
+ - Integration with holidays module for deduction and allowance
+ - Integration with hr_payroll_account for the automatic account_move_line
+ creation from the payslip
+ - Continue to integrate the contribution. Only the main contribution are
+ currently implemented
+ - Remake the report under webkit
+ - The payslip.line with appears_in_payslip = False should appears in the
+ payslip interface, but not in the payslip report
""",
'active': False,
diff --git a/addons/l10n_fr_hr_payroll/l10n_fr_hr_payroll_view.xml b/addons/l10n_fr_hr_payroll/l10n_fr_hr_payroll_view.xml
index 4bf8cdf018b..eb407917d81 100755
--- a/addons/l10n_fr_hr_payroll/l10n_fr_hr_payroll_view.xml
+++ b/addons/l10n_fr_hr_payroll/l10n_fr_hr_payroll_view.xml
@@ -11,15 +11,15 @@
-
-
+
+
-
+
diff --git a/addons/l10n_fr_rib/__openerp__.py b/addons/l10n_fr_rib/__openerp__.py
index ac1b03f4b3e..fedac46381d 100644
--- a/addons/l10n_fr_rib/__openerp__.py
+++ b/addons/l10n_fr_rib/__openerp__.py
@@ -25,17 +25,26 @@
"category": 'Hidden/Dependency',
'description': '''
This module lets users enter the banking details of Partners in the RIB format (French standard for bank accounts details).
-RIB Bank Accounts can be entered in the "Accounting" tab of the Partner form by specifying the account type "RIB". The four standard RIB fields will then become mandatory:
-- Bank Code
-- Office Code
-- Account number
-- RIB key
-As a safety measure, OpenERP will check the RIB key whenever a RIB is saved, and will refuse to record the data if the key is incorrect. Please bear in mind that this can only happen when the user presses the "save" button, for example on the Partner Form.
-Since each bank account may relate to a Bank, users may enter the RIB Bank Code in the Bank form - it will the pre-fill the Bank Code on the RIB when they select the Bank.
-To make this easier, this module will also let users find Banks using their RIB code.
+===========================================================================================================================
-The module base_iban can be a useful addition to this module, because French banks are now progressively adopting the international IBAN format instead of the RIB format.
-The RIB and IBAN codes for a single account can be entered by recording two Bank Accounts in OpenERP: the first with the type "RIB", the second with the type "IBAN".
+RIB Bank Accounts can be entered in the "Accounting" tab of the Partner form by specifying
+the account type "RIB". The four standard RIB fields will then become mandatory:
+ - Bank Code
+ - Office Code
+ - Account number
+ - RIB key
+As a safety measure, OpenERP will check the RIB key whenever a RIB is saved, and
+will refuse to record the data if the key is incorrect. Please bear in mind that
+this can only happen when the user presses the "save" button, for example on the
+Partner Form. Since each bank account may relate to a Bank, users may enter the
+RIB Bank Code in the Bank form - it will the pre-fill the Bank Code on the RIB
+when they select the Bank. To make this easier, this module will also let users
+find Banks using their RIB code.
+
+The module base_iban can be a useful addition to this module, because French banks
+are now progressively adopting the international IBAN format instead of the RIB format.
+The RIB and IBAN codes for a single account can be entered by recording two Bank
+Accounts in OpenERP: the first with the type "RIB", the second with the type "IBAN".
''',
'author' : u'Numérigraphe SARL',
'depends': ['account', 'base_iban'],
diff --git a/addons/l10n_fr_rib/bank_view.xml b/addons/l10n_fr_rib/bank_view.xml
index b3062f4c706..627c2eacf04 100644
--- a/addons/l10n_fr_rib/bank_view.xml
+++ b/addons/l10n_fr_rib/bank_view.xml
@@ -9,7 +9,7 @@
form
-
+
@@ -17,7 +17,7 @@
-
+
diff --git a/addons/l10n_gt/__openerp__.py b/addons/l10n_gt/__openerp__.py
index f05bbdaaff1..44389e68439 100644
--- a/addons/l10n_gt/__openerp__.py
+++ b/addons/l10n_gt/__openerp__.py
@@ -40,7 +40,9 @@
This is the base module to manage the accounting chart for Guatemala.
=====================================================================
-Agrega una nomenclatura contable para Guatemala. También icluye impuestos y la moneda del Quetzal. -- Adds accounting chart for Guatemala. It also includes taxes and the Quetzal currency""",
+Agrega una nomenclatura contable para Guatemala. También icluye impuestos y
+la moneda del Quetzal. -- Adds accounting chart for Guatemala. It also includes
+taxes and the Quetzal currency.""",
'author': 'José Rodrigo Fernández Menegazzo',
'website': 'http://solucionesprisma.com/',
'depends': ['base', 'account', 'account_chart'],
diff --git a/addons/l10n_hn/__openerp__.py b/addons/l10n_hn/__openerp__.py
index af8bb4cfa1d..25655e52e70 100644
--- a/addons/l10n_hn/__openerp__.py
+++ b/addons/l10n_hn/__openerp__.py
@@ -36,7 +36,10 @@
'name': 'Honduras - Accounting',
'version': '0.1',
'category': 'Localization/Account Charts',
- 'description': """Agrega una nomenclatura contable para Honduras. También incluye impuestos y la moneda Lempira. -- Adds accounting chart for Honduras. It also includes taxes and the Lempira currency""",
+ 'description': """
+Agrega una nomenclatura contable para Honduras. También incluye impuestos y la
+moneda Lempira. -- Adds accounting chart for Honduras. It also includes taxes
+and the Lempira currency.""",
'author': 'Salvatore Josue Trimarchi Pinto',
'website': 'http://trimarchi.co.cc',
'depends': ['base', 'account', 'account_chart'],
diff --git a/addons/l10n_in/__init__.py b/addons/l10n_in/__init__.py
index 19de03b504c..49a09e5570e 100644
--- a/addons/l10n_in/__init__.py
+++ b/addons/l10n_in/__init__.py
@@ -1,29 +1,21 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
##############################################################################
#
-# Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved.
-# Fabien Pinckaers
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
#
-# WARNING: This program as such is intended to be used by professional
-# programmers who take the whole responsability of assessing all potential
-# consequences resulting from its eventual inadequacies and bugs
-# End users who are looking for a ready-to-use solution with commercial
-# garantees and support are strongly adviced to contract a Free Software
-# Service Company
+# 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 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 2
-# 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.
#
-# 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, write to the Free Software
-# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
#
##############################################################################
diff --git a/addons/l10n_in/__openerp__.py b/addons/l10n_in/__openerp__.py
index c13139f6736..1e49509c627 100644
--- a/addons/l10n_in/__openerp__.py
+++ b/addons/l10n_in/__openerp__.py
@@ -1,8 +1,8 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
+# 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
@@ -20,11 +20,11 @@
##############################################################################
{
- "name": "India - Accounting",
+ "name": "Indian - Accounting",
"version": "1.0",
"description": """
-Indian Accounting : Chart of Account.
-=====================================
+Indian Accounting: Chart of Account.
+====================================
Indian accounting chart and localization.
""",
@@ -36,7 +36,11 @@ Indian accounting chart and localization.
],
"demo_xml": [],
"update_xml": [
- "l10n_in_chart.xml",
+ "l10n_in_tax_code_template.xml",
+ "l10n_in_public_chart.xml",
+ "l10n_in_public_tax_template.xml",
+ "l10n_in_private_chart.xml",
+ "l10n_in_private_tax_template.xml",
"l10n_in_wizard.xml",
],
"auto_install": False,
diff --git a/addons/l10n_in/account_tax.xml b/addons/l10n_in/account_tax.xml
deleted file mode 100644
index 5cba82b748f..00000000000
--- a/addons/l10n_in/account_tax.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
- PPn (10%)(10.0%)
- 0.100000
- percent
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/l10n_in/account_tax_code.xml b/addons/l10n_in/account_tax_code.xml
deleted file mode 100644
index bec870dd426..00000000000
--- a/addons/l10n_in/account_tax_code.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
- Tax balance to pay
-
-
-
- Tax Due (Tax to pay)
-
-
-
-
- Tax payable
-
-
-
-
- Tax bases
-
-
-
- Base of taxed sales
-
-
-
-
-
- Base of taxed purchases
-
-
-
-
-
diff --git a/addons/l10n_in/l10n_in_chart.xml b/addons/l10n_in/l10n_in_chart.xml
deleted file mode 100644
index d18d2b801e0..00000000000
--- a/addons/l10n_in/l10n_in_chart.xml
+++ /dev/null
@@ -1,447 +0,0 @@
-
-
-
-
-
-
-
- Indian Chart of Account
- 0
- view
-
-
-
-
-
- Balance Sheet
- IA_AC0
- view
-
-
-
-
-
-
- Assets
- IA_AC01
- view
-
-
-
-
-
-
- Current Assets
- IA_AC011
- view
-
-
-
-
-
-
- Bank Account
- IA_AC0111
- liquidity
-
-
-
-
-
-
- Cash In Hand Account
- IA_AC0112
- view
-
-
-
-
-
-
- Cash Account
- IA_AC01121
- view
-
-
-
-
-
-
- Deposit Account
- IA_AC0113
- other
-
-
-
-
-
-
- Loan & Advance(Assets) Account
- IA_AC0114
- other
-
-
-
-
-
-
- Total Sundry Debtors Account
- IA_AC0116
- view
-
-
-
-
-
-
- Sundry Debtors Account
- IA_AC01161
- receivable
-
-
-
-
-
-
- Fixed Assets
- IA_AC012
- other
-
-
-
-
-
-
- Investment
- IA_AC013
- other
-
-
-
-
-
-
- Misc. Expenses(Asset)
- IA_AC014
- other
-
-
-
-
-
-
- Liabilities
- IA_AC02
- view
-
-
-
-
-
-
- Current Liabilities
- IA_AC021
- view
-
-
-
-
-
-
- Duties & Taxes
- IA_AC0211
- other
-
-
-
-
-
-
- Provision
- IA_AC0212
- other
-
-
-
-
-
-
- Total Sundry Creditors
- IA_AC0213
- view
-
-
-
-
-
-
- Sundry Creditors Account
- IA_AC02131
- payable
-
-
-
-
-
-
- Branch/Division
- IA_AC022
- other
-
-
-
-
-
-
- Share Holder/Owner Fund
- IA_AC023
- view
-
-
-
-
-
-
- Capital Account
- IA_AC0231
- other
-
-
-
-
-
-
- Reserve and Profit/Loss Account
- IA_AC0232
- other
-
-
-
-
-
-
- Loan(Liability) Account
- IA_AC024
- view
-
-
-
-
-
-
- Bank OD Account
- IA_AC0241
- other
-
-
-
-
-
-
- Secured Loan Account
- IA_AC0242
- other
-
-
-
-
-
-
- Unsecured Loan Account
- IA_AC0243
- other
-
-
-
-
-
-
- Suspense Account
- IA_AC025
- other
-
-
-
-
-
-
-
- Profit And Loss Account
- IA_AC1
- view
-
-
-
-
-
-
- Expense
- IA_AC11
- view
-
-
-
-
-
-
- Direct Expenses
- IA_AC111
- other
-
-
-
-
-
-
- Indirect Expenses
- IA_AC112
- other
-
-
-
-
-
-
- Purchase
- IA_AC113
- other
-
-
-
-
-
-
- Opening Stock
- IA_AC114
- other
-
-
-
-
-
- Salary Expenses
- IA_AC115
- other
-
-
-
-
-
-
-
- Income
- IA_AC12
- view
-
-
-
-
-
-
- Direct Incomes
- IA_AC121
- other
-
-
-
-
-
-
- Indirect Incomes
- IA_AC122
- other
-
-
-
-
-
-
- Sales Account
- IA_AC123
- other
-
-
-
-
-
- Goods Given Account
- IA_AC124
- other
-
-
-
-
-
-
-
-
- Tax
-
-
-
- Tax Balance to Pay
-
-
-
-
- Tax Due (Tax to pay)
-
-
-
-
- Tax Payable
-
-
-
-
- Tax Bases
-
-
-
-
-
- Base of Taxed Sales
-
-
-
-
-
- Base of Taxed Purchases
-
-
-
-
- OPJ
- Opening Journal
- situation
-
-
-
-
- India - Chart of Accounts
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/l10n_in/l10n_in_private_chart.xml b/addons/l10n_in/l10n_in_private_chart.xml
new file mode 100644
index 00000000000..db11d0638c8
--- /dev/null
+++ b/addons/l10n_in/l10n_in_private_chart.xml
@@ -0,0 +1,523 @@
+
+
+
+
+
+
+
+ Partnership/Private Firm Chart of Account
+ 0
+ view
+
+
+
+
+
+
+ Balance Sheet
+ 1
+ view
+
+
+
+
+
+
+
+
+ Assets
+ 10
+ view
+
+
+
+
+
+
+ Cash
+ 101
+ liquidity
+
+
+
+ Checking account balance (as shown in company records), currency, coins, checks received from customers but not yet deposited.
+
+
+
+ Accounts Receivable
+ 120
+ receivable
+
+
+
+ Amounts owed to the company for services performed or products sold but not yet paid for.
+
+
+
+ Merchandise Inventory
+ 140
+ other
+
+
+
+ Cost of merchandise purchased but has not yet been sold.
+
+
+
+ Supplies
+ 150
+ other
+
+
+
+ Cost of supplies that have not yet been used. Supplies that have been used are recorded in Supplies Expense.
+
+
+
+ Prepaid Insurance
+ 160
+ other
+
+
+
+ Cost of insurance that is paid in advance and includes a future accounting period.
+
+
+
+ Land
+ 170
+ other
+
+
+
+ Cost to acquire and prepare land for use by the company.
+
+
+
+ Buildings
+ 175
+ other
+
+
+
+ Cost to purchase or construct buildings for use by the company.
+
+
+
+ Accumulated Depreciation - Buildings
+ 178
+ other
+
+
+
+ Amount of the buildings' cost that has been allocated to Depreciation Expense since the time the building was acquired.
+
+
+
+ Equipment
+ 180
+ other
+
+
+
+ Cost to acquire and prepare equipment for use by the company.
+
+
+
+ Accumulated Depreciation - Equipment
+ 188
+ other
+
+
+
+ Amount of equipment's cost that has been allocated to Depreciation Expense since the time the equipment was acquired.
+
+
+
+ Tax Receivable
+ 189
+ other
+
+
+
+
+
+
+
+ Liabilities
+ 20
+ view
+
+
+
+
+
+
+ Notes Payable
+ 210
+ other
+
+
+
+ The amount of principal due on a formal written promise to pay. Loans from banks are included in this account.
+
+
+
+ Accounts Payable
+ 215
+ payable
+
+
+
+ Amount owed to suppliers who provided goods and services to the company but did not require immediate payment in cash.
+
+
+
+ Wages Payable
+ 220
+ other
+
+
+
+ Amount owed to employees for hours worked but not yet paid.
+
+
+
+ Interest Payable
+ 230
+ other
+
+
+
+ Amount owed for interest on Notes Payable up until the date of the balance sheet. This is computed by multiplying the amount of the note times the effective interest rate times the time period.
+
+
+
+ Unearned Revenues
+ 240
+ other
+
+
+
+ Amounts received in advance of delivering goods or providing services. When the goods are delivered or services are provided, this liability amount decreases.
+
+
+
+ Mortgage Loan Payable
+ 250
+ other
+
+
+
+ A formal loan that involves a lien on real estate until the loan is repaid.
+
+
+
+ Reserve and Surplus Account
+ 260
+ other
+
+
+
+ A Reserve and Surplus Account.
+
+
+
+
+
+
+ Tax payable
+ 216
+ view
+
+
+
+
+
+
+ Sales Tax Payable
+ 2161
+ other
+
+
+
+
+
+
+ VAT Payable
+ 2162
+ other
+
+
+
+
+
+
+ Service Tax Payable
+ 2163
+ other
+
+
+
+
+
+
+ Exice Duty Payable
+ 2164
+ other
+
+
+
+
+
+
+
+
+ Owner's Equity Accounts
+ 29
+ view
+
+
+
+
+
+
+
+
+
+ Profit And Loss
+ 3
+ view
+
+
+
+
+
+
+
+
+ Income
+ 30
+ view
+
+
+
+
+
+
+
+
+ Operating Revenue Accounts
+ 31
+ view
+
+
+
+
+
+
+ Service Revenues
+ 310
+ other
+
+
+
+ Amounts earned from providing services to clients, either for cash or on credit. When a service is provided on credit, both this account and Accounts Receivable will increase. When a service is provided for immediate cash, both this account and Cash will increase.
+
+
+
+ Product Sales
+ 311
+ other
+
+
+
+ Sales of product account
+
+
+
+ Non-Operating Revenue and Gains
+ 80
+ view
+
+
+
+
+
+
+ Interest Revenues
+ 810
+ other
+
+
+
+ Interest and dividends earned on bank accounts, investments or notes receivable. This account is increased when the interest is earned and either Cash or Interest Receivable is also increased.
+
+
+
+ Gain on Sale of Assets
+ 811
+ other
+
+
+
+ Occurs when the company sells one of its assets (other than inventory) for more than the asset's book value.
+
+
+
+
+
+ Expense
+ 50
+ view
+
+
+
+
+
+
+ Operating Expense Accounts
+ 51
+ view
+
+
+
+
+
+
+ Salaries Expense
+ 500
+ other
+
+
+
+ Expenses incurred for the work performed by salaried employees during the accounting period. These employees normally receive a fixed amount on a weekly, monthly, or annual basis.
+
+
+
+ Wages Expense
+ 510
+ other
+
+
+
+ Expenses incurred for the work performed by non-salaried employees during the accounting period. These employees receive an hourly rate of pay.
+
+
+
+ Supplies Expense
+ 540
+ other
+
+
+
+ Cost of supplies used up during the accounting period.
+
+
+
+ Rent Expense
+ 560
+ other
+
+
+
+ Cost of occupying rented facilities during the accounting period.
+
+
+
+ Utilities Expense
+ 570
+ other
+
+
+
+ Costs for electricity, heat, water, and sewer that were used during the accounting period.
+
+
+
+ Telephone Expense
+ 576
+ other
+
+
+
+ Cost of telephone used during the current accounting period.
+
+
+
+ Advertising Expense
+ 610
+ other
+
+
+
+ Costs incurred by the company during the accounting period for ads, promotions, and other selling and expenses (other than salaries).
+
+
+
+ Depreciation Expense
+ 750
+ other
+
+
+
+ Cost of long-term assets allocated to expense during the current accounting period.
+
+
+
+
+
+ Non-Operating Expenses and Losses
+ 90
+ view
+
+
+
+
+
+
+ Loss on Sale of Assets
+ 960
+ other
+
+
+
+ Occurs when the company sells one of its assets (other than inventory) for less than the asset's book value.
+
+
+
+
+
+ India - Chart of Accounts for Private Ltd/Partnership
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/l10n_in/l10n_in_private_tax_template.xml b/addons/l10n_in/l10n_in_private_tax_template.xml
new file mode 100644
index 00000000000..7aec30f0575
--- /dev/null
+++ b/addons/l10n_in/l10n_in_private_tax_template.xml
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+ Sale Tax-15%
+
+ 0.15
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+
+
+ Sale Tax-12%
+
+ 0.12
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+
+
+ Sale Tax-4%
+
+ 0.04
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Purchase Tax-15%
+
+
+
+ 0.15
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
+ VAT-5%(4% VAT+1% Add. Tax.)
+
+ 0.05
+ percent
+ all
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ VAT-15%(12.5% VAT+2.5% Add. Tax.)
+
+ 0.15
+ percent
+ all
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ VAT-8%
+
+ 0.08
+ percent
+ all
+
+
+
+
+
+
+
+
+
+
+
+ VAT-10%
+
+ 0.10
+ percent
+ all
+
+
+
+
+
+
+
+
+
+
+
+ VAT-12.5%
+
+ 12.5
+ percent
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Excise Duty-10.30%
+
+ 0.10
+ percent
+ sale
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ Excise Duty-2%
+ 0.02
+ percent
+ sale
+
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+ Excise Duty-1%
+ 0.01
+ percent
+ sale
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+
+
+ all
+ Service Tax-12.30%
+
+ 0.12
+ percent
+
+
+
+
+
+
+
+
+
+
+
+ Service Tax-%2
+ 0.02
+ percent
+ all
+
+
+
+
+
+
+
+
+
+
+
+ Service Tax-%1
+ 0.01
+ percent
+ all
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/l10n_in/l10n_in_public_chart.xml b/addons/l10n_in/l10n_in_public_chart.xml
new file mode 100644
index 00000000000..2c2dda0dd9d
--- /dev/null
+++ b/addons/l10n_in/l10n_in_public_chart.xml
@@ -0,0 +1,676 @@
+
+
+
+
+
+
+
+
+ Public Firm Chart of Account
+ 0
+ view
+
+
+
+
+
+
+ Balance Sheet
+ 1
+ view
+
+
+
+
+
+
+
+ Assets
+ 10
+ view
+
+
+
+
+
+
+ Current Assets
+ 10000
+ view
+
+
+
+
+
+
+ Cash - Regular Checking
+ 10100
+ liquidity
+
+
+
+
+
+
+ Cash - Payroll Checking
+ 10200
+ liquidity
+
+
+
+
+
+
+ Petty Cash Fund
+ 10600
+ liquidity
+
+
+
+
+
+
+ Accounts Receivable
+ 12100
+ receivable
+
+
+
+
+
+
+ Allowance for Doubtful Accounts
+ 12500
+ other
+
+
+
+
+
+
+ Inventory
+ 13100
+ other
+
+
+
+
+
+
+ Supplies
+ 14100
+ other
+
+
+
+
+
+
+ Prepaid Insurance
+ 15300
+ other
+
+
+
+
+
+
+ Tax Receivable
+ 15400
+ other
+
+
+
+
+
+
+
+
+ Property, Plant, and Equipment
+ 17000
+ view
+
+
+
+
+
+
+ Land
+ 17200
+ other
+
+
+
+
+
+
+ Buildings
+ 17100
+ other
+
+
+
+
+
+
+ Equipment
+ 17300
+ other
+
+
+
+
+
+
+ Vehicles
+ 17800
+ other
+
+
+
+
+
+
+ Accumulated Depreciation - Buildings
+ 18100
+ other
+
+
+
+
+
+
+ Accumulated Depreciation - Equipment
+ 18300
+ other
+
+
+
+
+
+
+ Accumulated Depreciation - Vehicles
+ 18800
+ other
+
+
+
+
+
+
+
+
+ Liabilities
+ 20
+ view
+
+
+
+
+
+
+ Current Liabilities
+ 20000
+ view
+
+
+
+
+
+
+ Notes Payable - Credit Line #1
+ 20100
+ other
+
+
+
+
+
+
+ Notes Payable - Credit Line #2
+ 20200
+ other
+
+
+
+
+
+
+ Accounts Payable
+ 21000
+ payable
+
+
+
+
+
+
+ Wages Payable
+ 22100
+ other
+
+
+
+
+
+
+ Interest Payable
+ 23100
+ other
+
+
+
+
+
+
+ Unearned Revenues
+ 24500
+ other
+
+
+
+
+
+
+ Long-term Liabilities
+ 25000
+ view
+
+
+
+
+
+
+ Mortgage Loan Payable
+ 25100
+ other
+
+
+
+
+
+
+ Bonds Payable
+ 25600
+ other
+
+
+
+
+
+
+ Discount on Bonds Payable
+ 25650
+ other
+
+
+
+
+
+
+ Stockholders' Equity
+ 27000
+ view
+
+
+
+
+
+
+ Common Stock, No Par
+ 27100
+ other
+
+
+
+
+
+
+ Retained Earnings
+ 27500
+ other
+
+
+
+
+
+
+ Treasury Stock
+ 29500
+ other
+
+
+
+
+
+
+ Reserve and Surplus Account
+ 24600
+ other
+
+
+
+
+
+
+
+
+ Tax payable
+ 24700
+ view
+
+
+
+
+
+
+ Sales Tax Payable
+ 24710
+ other
+
+
+
+
+
+
+ VAT Payable
+ 24720
+ other
+
+
+
+
+
+
+ Exice Duty Payable
+ 24730
+ other
+
+
+
+
+
+
+ Service Tax Payable
+ 24740
+ other
+
+
+
+
+
+
+
+
+ Profit And Loss
+ 3
+ view
+
+
+
+
+
+
+
+
+ Income
+ 30
+ view
+
+
+
+
+
+
+
+
+ Operating Revenues
+ 30000
+ view
+
+
+
+
+
+
+ Sales - Division #1, Product Line 010
+ 31010
+ other
+
+
+
+
+
+
+ Sales - Division #1, Product Line 022
+ 31022
+ other
+
+
+
+
+
+
+ Sales - Division #2, Product Line 015
+ 32015
+ other
+
+
+
+
+
+
+ Sales - Division #3, Product Line 110
+ 33110
+ other
+
+
+
+
+
+
+ Non-Operating Revenue and Gains
+ 90000
+ view
+
+
+
+
+
+
+ Gain on Sale of Assets
+ 91800
+ other
+
+
+
+
+
+
+
+
+ Expense
+ 40
+ view
+
+
+
+
+
+
+
+
+
+ Cost of Goods Sold
+ 40000
+ view
+
+
+
+
+
+
+
+ COGS - Division #1, Product Line 010
+ 41010
+ other
+
+
+
+
+
+
+ COGS - Division #1, Product Line 022
+ 41022
+ other
+
+
+
+
+
+
+ COGS - Division #2, Product Line 015
+ 42015
+ other
+
+
+
+
+
+
+ COGS - Division #3, Product Line 110
+ 43110
+ other
+
+
+
+
+
+
+
+
+ Marketing Expenses
+ 50000
+ view
+
+
+
+
+
+
+ Marketing Dept. Salaries
+ 50100
+ other
+
+
+
+
+
+
+ Marketing Dept. Payroll Taxes
+ 50150
+ other
+
+
+
+
+
+
+ Marketing Dept. Supplies
+ 50200
+ other
+
+
+
+
+
+
+ Marketing Dept. Telephone
+ 50600
+ other
+
+
+
+
+
+
+
+
+ Payroll Dept. Expenses
+ 59000
+ view
+
+
+
+
+
+
+ Payroll Dept. Salaries
+ 59100
+ other
+
+
+
+
+
+
+ Payroll Dept. Payroll Taxes
+ 59150
+ other
+
+
+
+
+
+
+ Payroll Dept. Supplies
+ 59200
+ other
+
+
+
+
+
+
+ Payroll Dept. Telephone
+ 59600
+ other
+
+
+
+
+
+
+
+
+ Non-Operating Expenses and Losses
+ 96000
+ view
+
+
+
+
+
+
+ Loss on Sale of Assets
+ 96100
+ other
+
+
+
+
+
+
+ India - Chart of Accounts for Public Ltd
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/l10n_in/l10n_in_public_tax_template.xml b/addons/l10n_in/l10n_in_public_tax_template.xml
new file mode 100644
index 00000000000..03dbfa7cc75
--- /dev/null
+++ b/addons/l10n_in/l10n_in_public_tax_template.xml
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+ Sale Tax-15%
+
+
+
+ 0.15
+ percent
+ sale
+
+
+
+
+
+
+
+
+ Sale Tax-12%
+
+
+
+ 0.12
+ percent
+ sale
+
+
+
+
+
+
+
+
+ Sale Tax-4%
+
+
+
+ 0.04
+ percent
+ sale
+
+
+
+
+
+
+
+
+
+
+ Purchase Tax-15%
+
+
+
+ 0.15
+ percent
+ purchase
+
+
+
+
+
+
+
+
+
+
+
+
+ VAT-5%(4% VAT+1% Add. Tax.)
+
+
+
+ 0.05
+ percent
+ all
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ VAT-15% (12.5% VAT + 2.5% Add. Tax.)
+
+
+
+ 0.15
+ percent
+ all
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ VAT-8%
+
+
+
+ 0.08
+ percent
+ all
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ VAT-10%
+
+
+
+ 0.10
+ percent
+ all
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ VAT-12.5%
+
+
+
+ 12.5
+ percent
+ all
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+
+
+ Service Tax-12.30%
+
+
+
+ 0.12
+ percent
+ all
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+ Service Tax-%2
+
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+ 0.02
+ percent
+ all
+
+
+
+
+
+ Service Tax-%1
+ 0.01
+
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+ percent
+ all
+
+
+
+
+
+
+
+ Excise Duty-10.30%
+
+
+
+ 0.10
+ percent
+ sale
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ Excise Duty-2%
+ 0.02
+ percent
+ sale
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+ Excise Duty-1%
+ 0.01
+ percent
+ sale
+
+
+
+ 1
+
+ 1
+
+ 1
+
+ 1
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/l10n_in/l10n_in_tax_code_template.xml b/addons/l10n_in/l10n_in_tax_code_template.xml
new file mode 100644
index 00000000000..94217d79c3c
--- /dev/null
+++ b/addons/l10n_in/l10n_in_tax_code_template.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ Tax
+
+
+
+ Tax Balance to Pay
+
+
+
+
+ Tax Received
+
+
+
+
+ Tax Paid
+
+
+
+
+ Tax Bases
+
+
+
+
+
+ Base of Taxed Sales
+
+
+
+
+ Base of Taxed Purchases
+
+
+
+
+
diff --git a/addons/l10n_in/l10n_in_wizard.xml b/addons/l10n_in/l10n_in_wizard.xml
index 9c53722876b..a877b8b577e 100644
--- a/addons/l10n_in/l10n_in_wizard.xml
+++ b/addons/l10n_in/l10n_in_wizard.xml
@@ -1,3 +1,4 @@
+
diff --git a/addons/l10n_lu/__openerp__.py b/addons/l10n_lu/__openerp__.py
index bb12abd76fc..1ade34faaca 100644
--- a/addons/l10n_lu/__openerp__.py
+++ b/addons/l10n_lu/__openerp__.py
@@ -28,7 +28,7 @@
This is the base module to manage the accounting chart for Luxembourg.
======================================================================
- * the KLUWER Chart of Accounts,
+ * the KLUWER Chart of Accounts
* the Tax Code Chart for Luxembourg
* the main taxes used in Luxembourg""",
'author': 'OpenERP SA',
diff --git a/addons/l10n_ma/__openerp__.py b/addons/l10n_ma/__openerp__.py
index 21b7c632d6b..52f453af8bb 100644
--- a/addons/l10n_ma/__openerp__.py
+++ b/addons/l10n_ma/__openerp__.py
@@ -28,7 +28,11 @@
This is the base module to manage the accounting chart for Maroc.
=================================================================
-Ce Module charge le modèle du plan de comptes standard Marocain et permet de générer les états comptables aux normes marocaines (Bilan, CPC (comptes de produits et charges), balance générale à 6 colonnes, Grand livre cumulatif...). L'intégration comptable a été validé avec l'aide du Cabinet d'expertise comptable Seddik au cours du troisième trimestre 2010""",
+Ce Module charge le modèle du plan de comptes standard Marocain et permet de
+générer les états comptables aux normes marocaines (Bilan, CPC (comptes de
+produits et charges), balance générale à 6 colonnes, Grand livre cumulatif...).
+L'intégration comptable a été validé avec l'aide du Cabinet d'expertise comptable
+Seddik au cours du troisième trimestre 2010.""",
"website": "http://www.kazacube.com",
"depends" : ["base", "account"],
"init_xml" : [],
diff --git a/addons/l10n_multilang/__openerp__.py b/addons/l10n_multilang/__openerp__.py
index 42ca20a2f8e..cf521f124f4 100644
--- a/addons/l10n_multilang/__openerp__.py
+++ b/addons/l10n_multilang/__openerp__.py
@@ -25,10 +25,11 @@
"author" : "OpenERP SA",
"category": 'Hidden/Dependency',
"description": """
- * Multi language support for Chart of Accounts, Taxes, Tax Codes , Journals, Accounting Templates,
- Analytic Chart of Accounts and Analytic Journals.
+ * Multi language support for Chart of Accounts, Taxes, Tax Codes, Journals,
+ Accounting Templates, Analytic Chart of Accounts and Analytic Journals.
* Setup wizard changes
- - Copy translations for COA, Tax, Tax Code and Fiscal Position from templates to target objects.
+ - Copy translations for COA, Tax, Tax Code and Fiscal Position from
+ templates to target objects.
""",
'website': 'http://www.openerp.com',
'init_xml': [],
diff --git a/addons/l10n_nl/__openerp__.py b/addons/l10n_nl/__openerp__.py
index 562fd0ab876..8b9cc3de451 100644
--- a/addons/l10n_nl/__openerp__.py
+++ b/addons/l10n_nl/__openerp__.py
@@ -94,19 +94,25 @@ This is the module to manage the accounting chart for Netherlands in OpenERP.
=============================================================================
Read changelog in file __openerp__.py for version information.
-Dit is een basismodule om een uitgebreid grootboek- en BTW schema voor Nederlandse bedrijven te installeren in OpenERP versie 5.
+Dit is een basismodule om een uitgebreid grootboek- en BTW schema voor
+Nederlandse bedrijven te installeren in OpenERP versie 5.
-De BTW rekeningen zijn waar nodig gekoppeld om de juiste rapportage te genereren, denk b.v. aan intracommunautaire verwervingen
-waarbij u 19% BTW moet opvoeren, maar tegelijkertijd ook 19% als voorheffing weer mag aftrekken.
+De BTW rekeningen zijn waar nodig gekoppeld om de juiste rapportage te genereren,
+denk b.v. aan intracommunautaire verwervingen waarbij u 19% BTW moet opvoeren,
+maar tegelijkertijd ook 19% als voorheffing weer mag aftrekken.
Na installatie van deze module word de configuratie wizard voor "Accounting" aangeroepen.
- * U krijgt een lijst met grootboektemplates aangeboden waarin zich ook het Nederlandse grootboekschema bevind.
+ * U krijgt een lijst met grootboektemplates aangeboden waarin zich ook het
+ Nederlandse grootboekschema bevind.
- * Als de configuratie wizard start, wordt u gevraagd om de naam van uw bedrijf in te voeren, welke grootboekschema te installeren, uit hoeveel cijfers een grootboekrekening mag bestaan, het rekeningnummer van uw bank en de currency om Journalen te creeren.
+ * Als de configuratie wizard start, wordt u gevraagd om de naam van uw bedrijf
+ in te voeren, welke grootboekschema te installeren, uit hoeveel cijfers een
+ grootboekrekening mag bestaan, het rekeningnummer van uw bank en de currency
+ om Journalen te creeren.
-Let op!! -> De template van het Nederlandse rekeningschema is opgebouwd uit 4 cijfers. Dit is het minimale aantal welk u moet invullen, u mag het aantal verhogen. De extra cijfers worden dan achter het rekeningnummer aangevult met "nullen"
-
- * Dit is dezelfe configuratie wizard welke aangeroepen kan worden via Financial Management/Configuration/Financial Accounting/Financial Accounts/Generate Chart of Accounts from a Chart Template.
+Let op!! -> De template van het Nederlandse rekeningschema is opgebouwd uit 4
+cijfers. Dit is het minimale aantal welk u moet invullen, u mag het aantal verhogen.
+De extra cijfers worden dan achter het rekeningnummer aangevult met "nullen".
""",
"author" : "Veritos - Jan Verlaan",
diff --git a/addons/l10n_pe/__openerp__.py b/addons/l10n_pe/__openerp__.py
index fa65c1033b7..7908664ca83 100644
--- a/addons/l10n_pe/__openerp__.py
+++ b/addons/l10n_pe/__openerp__.py
@@ -22,9 +22,10 @@
"name": "Peru Localization Chart Account",
"version": "1.0",
"description": """
-Peruvian accounting chart and tax localization. Acording the PCGE 2010
+Peruvian accounting chart and tax localization. Acording the PCGE 2010.
-Plan contable peruano e impuestos de acuerdo a disposiciones vigentes de la SUNAT 2011 (PCGE 2010)
+Plan contable peruano e impuestos de acuerdo a disposiciones vigentes de la
+SUNAT 2011 (PCGE 2010).
""",
"author": ["Cubic ERP"],
diff --git a/addons/l10n_syscohada/__openerp__.py b/addons/l10n_syscohada/__openerp__.py
index 01c1aaef96f..2fd6917ec11 100644
--- a/addons/l10n_syscohada/__openerp__.py
+++ b/addons/l10n_syscohada/__openerp__.py
@@ -24,12 +24,15 @@
"version" : "1.0",
"author" : "Baamtu Senegal",
"category" : "Localization/Account Charts",
- "description": """This module implements the accounting chart for OHADA area.
- It allows any company or association to manage its financial accounting.
- Countries that use OHADA are the following:
+ "description": """
+This module implements the accounting chart for OHADA area.
+===========================================================
+
+It allows any company or association to manage its financial accounting.
+Countries that use OHADA are the following:
Benin, Burkina Faso, Cameroon, Central African Republic, Comoros, Congo,
- Ivory Coast, Gabon, Guinea, Guinea Bissau,
- Equatorial Guinea, Mali, Niger, Replica of Democratic Congo, Senegal, Chad, Togo.
+ Ivory Coast, Gabon, Guinea, Guinea Bissau, Equatorial Guinea, Mali, Niger,
+ Replica of Democratic Congo, Senegal, Chad, Togo.
""",
"website": "http://www.baamtu.com",
"depends" : ["account", "base_vat"],
diff --git a/addons/l10n_tr/__openerp__.py b/addons/l10n_tr/__openerp__.py
index ad4621499d3..d72865a216c 100644
--- a/addons/l10n_tr/__openerp__.py
+++ b/addons/l10n_tr/__openerp__.py
@@ -23,10 +23,11 @@
'category': 'Localization/Account Charts',
'description': """
Türkiye için Tek düzen hesap planı şablonu OpenERP Modülü.
-==============================================================================
+==========================================================
Bu modül kurulduktan sonra, Muhasebe yapılandırma sihirbazı çalışır
- * Sihirbaz sizden hesap planı şablonu, planın kurulacağı şirket,banka hesap bilgileriniz,ilgili para birimi gibi bilgiler isteyecek.
+ * Sihirbaz sizden hesap planı şablonu, planın kurulacağı şirket, banka hesap
+ bilgileriniz, ilgili para birimi gibi bilgiler isteyecek.
""",
'author': 'Ahmet Altınışık',
'maintainer':'https://launchpad.net/~openerp-turkey',
diff --git a/addons/l10n_uk/__openerp__.py b/addons/l10n_uk/__openerp__.py
index f6e8212aed0..444ea2b9dd3 100644
--- a/addons/l10n_uk/__openerp__.py
+++ b/addons/l10n_uk/__openerp__.py
@@ -23,7 +23,9 @@
'name': 'UK - Accounting',
'version': '1.0',
'category': 'Localization/Account Charts',
- 'description': """This is the latest UK OpenERP localisation necesary to run OpenERP accounting for UK SME's with:
+ 'description': """
+This is the latest UK OpenERP localisation necessary to run OpenERP accounting
+for UK SME's with:
- a CT600-ready chart of accounts
- VAT100-ready tax structure
- InfoLogic UK counties listing
diff --git a/addons/l10n_us/__openerp__.py b/addons/l10n_us/__openerp__.py
index 4aac9b14287..f900c3d5e99 100644
--- a/addons/l10n_us/__openerp__.py
+++ b/addons/l10n_us/__openerp__.py
@@ -24,7 +24,7 @@
"author": "OpenERP SA",
"category": 'Localization/Account Charts',
"description": """
- United States - Chart of accounts
+United States - Chart of accounts.
""",
'website': 'http://www.openerp.com',
'init_xml': [],
diff --git a/addons/l10n_uy/__openerp__.py b/addons/l10n_uy/__openerp__.py
index 4edfe22e1e8..5e4eb51ffc0 100644
--- a/addons/l10n_uy/__openerp__.py
+++ b/addons/l10n_uy/__openerp__.py
@@ -28,10 +28,10 @@
"category" : "Localization/Account Charts",
"website" : "https://launchpad.net/openerp-uruguay",
"description": """
-General Chart of Accounts
-=========================
+General Chart of Accounts.
+==========================
-Provide Templates for Chart of Accounts, Taxes for Uruguay
+Provide Templates for Chart of Accounts, Taxes for Uruguay.
""",
"license" : "AGPL-3",
diff --git a/addons/lunch/__openerp__.py b/addons/lunch/__openerp__.py
index 4ce4eaf3d6b..ad476588e93 100644
--- a/addons/lunch/__openerp__.py
+++ b/addons/lunch/__openerp__.py
@@ -22,22 +22,15 @@
{
"name": "Lunch Orders",
"author": "OpenERP SA",
- "Description": """
- The lunch module is for keeping a record of the order placed and payment of the orders.
- =======================================================================================
-
- The products are defined under categories and the payment records are maintained user-wise.
- Every user has a cashbox which keeps track of the amount paid for a particular order.
-
- """,
"version": "0.1",
"depends": ["base_tools"],
"category" : "Tools",
'description': """
- The base module to manage lunch
+The base module to manage lunch.
+================================
- keep track for the Lunch Order ,Cash Moves ,CashBox ,Product.
- Apply Different Category for the product.
+keep track for the Lunch Order, Cash Moves, CashBox, Product. Apply Different
+Category for the product.
""",
"init_xml": [],
"update_xml": [
diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py
index e911f24e809..ee22b0c153f 100644
--- a/addons/mail/__openerp__.py
+++ b/addons/mail/__openerp__.py
@@ -24,23 +24,24 @@
'version': '1.0',
'category':'Social Network',
"sequence": 2,
+ "summary": "Discussions, Feeds, Alerts",
'description': """
A bussiness oriented Social Networking with a fully-integrated email
and message management.
=====================================================================
+
The Social Networking module provides an unified social network
abstraction layer allowing applications to display a complete
-communication history on documents.It gives the users the possibility
+communication history on documents. It gives the users the possibility
to read and send messages and emails in an unified way.
It also provides a feeds page combined to a subscription mechanism, that
allows to follow documents, and to be constantly updated about recent
news.
-The main features of the module are :
-
+The main features of the module are:
* a clean and renewed communication history for any OpenERP
- document that can act as a discussion topic,
+ document that can act as a discussion topic,
* a discussion mean on documents,
* a subscription mechanism to be updated about new messages on
interesting documents,
@@ -66,11 +67,11 @@ The main features of the module are :
'mail_thread_view.xml',
'mail_group_view.xml',
'res_partner_view.xml',
+ 'res_users_view.xml',
+ 'data/mail_data.xml',
+ 'data/mail_group_data.xml',
'security/mail_security.xml',
'security/ir.model.access.csv',
- 'mail_data.xml',
- 'mail_group_data.xml',
- 'res_users_view.xml',
],
'installable': True,
'auto_install': False,
@@ -83,13 +84,15 @@ The main features of the module are :
'static/src/img/email_icong.png',
'static/src/img/_al.png',
'static/src/img/_pincky.png',
- 'static/src/img/feeds.png',
- 'static/src/img/feeds-hover.png',
'static/src/img/groupdefault.png',
+ 'static/src/img/attachment.png',
+ 'static/src/img/checklist.png',
+ 'static/src/img/formatting.png',
],
'css': [
'static/src/css/mail.css',
'static/src/css/mail_group.css',
+ 'static/src/css/mail_compose_message.css',
],
'js': [
'static/lib/jquery.expander/jquery.expander.js',
@@ -99,7 +102,7 @@ The main features of the module are :
'static/src/xml/mail.xml',
],
'demo': [
- 'mail_demo.xml',
+ 'data/mail_demo.xml',
],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_data.xml b/addons/mail/data/mail_data.xml
similarity index 100%
rename from addons/mail/mail_data.xml
rename to addons/mail/data/mail_data.xml
diff --git a/addons/mail/mail_demo.xml b/addons/mail/data/mail_demo.xml
similarity index 93%
rename from addons/mail/mail_demo.xml
rename to addons/mail/data/mail_demo.xml
index 1adec42a910..274259e544e 100644
--- a/addons/mail/mail_demo.xml
+++ b/addons/mail/data/mail_demo.xml
@@ -1,6 +1,6 @@
-
+
A cool attachment
@@ -28,7 +28,7 @@
Internal company announcemail.group
- html
+ htmlsit amet, consectetur adipiscing elit. Pellentesque et quam sapien, in sagittis tellus.
Praesent vel massa sed massa consequat egestas in tristique orci. Praesent iaculis libero et neque vehicula iaculis. Vivamus placerat tincidunt orci ac ornare. Proin ut dolor fringilla velit ultricies consequat. Maecenas sit amet ipsum non leo interdum imperdiet. Donec sapien mi, varius a consequat id, consectetur sit amet nulla.
@@ -41,10 +41,9 @@ Nulla turpis leo, rhoncus ut egestas sit amet, consectetur vitae urna. Mauris in
- Replymail.group
- html
+ htmltremendous blogpost ! (first comment)]]>comment
@@ -52,10 +51,9 @@ Nulla turpis leo, rhoncus ut egestas sit amet, consectetur vitae urna. Mauris in
- Replymail.group
- html
+ html
@@ -75,10 +73,9 @@ Would it be possible to learn more about the author ? (second comment)]]>
- Replymail.group
- html
+ html
+
+
+
+
+ All Company
+ All company users can come here and discuss.
+
+
+ Sales
+
+
+ All Employees
+
+
+
+
+
+
+
+ Module Social Network has been installed
+ Manage all communications on your documents (emails,
+ notifications, comments and discussions) and keep track of
+ news on documents you follow.
+
+
+
+
+
diff --git a/addons/mail/doc/mail_message.rst b/addons/mail/doc/mail_message.rst
index e41a772cac8..56244e251bf 100644
--- a/addons/mail/doc/mail_message.rst
+++ b/addons/mail/doc/mail_message.rst
@@ -3,22 +3,26 @@
mail.message
============
-TODO
+Models
++++++++
-mail.group
-++++++++++
+``mail.message.common`` is an abstract class for holding the main attributes of a
+message object. It could be reused as parent model for any database model
+or wizard screen that needs to hold a kind of message.
-A mail_group is a collection of users sharing messages in a discussion group. Group users are users that follow the mail group, using the subscription/follow mechanism of OpenSocial. A mail group has nothing in common wih res.users.group.
-Additional information on fields:
+All internal logic should be in a database-based model while this model
+holds the basics of a message. For example, a wizard for writing emails
+should inherit from this class and not from mail.message.
- - ``member_ids``: user member of the groups are calculated with ``message_get_subscribers`` method from mail.thread
- - ``member_count``: calculated with member_ids
- - ``is_subscriber``: calculated with member_ids
-res.users
-+++++++++
+.. versionchanged:: 7.0
-OpenChatter updates the res.users class:
- - it adds a preference about sending emails when receiving a notification
- - make a new user follow itself automatically
- - create a welcome message when creating a new user, to make his arrival in OpenERP more friendly
+ - ``subtype`` is renamed to ``content_subtype``: usually 'html' or 'plain'.
+ This field is used to select plain-text or rich-text contents accordingly.
+ - ``subtype`` is moved to mail.message model. The purpose is to be able to
+ distinguish message of the same type, such as notifications about creating
+ or cancelling a record. For example, it is used to add the possibility
+ to hide notifications in the wall.
+
+Those changes aim at being able to distinguish the message content to the
+message itself.
diff --git a/addons/mail/doc/mail_thread.rst b/addons/mail/doc/mail_thread.rst
index f15f78156d3..41ea014b06c 100644
--- a/addons/mail/doc/mail_thread.rst
+++ b/addons/mail/doc/mail_thread.rst
@@ -3,7 +3,25 @@
mail.thread and OpenChatter
===========================
-TODO
+API
++++
+
+Writing messages and notifications
+----------------------------------
+
+``message_append``
+
+Creates a new mail.message through message_create. The new message is attached
+to the current mail.thread, containing all the details passed as parameters.
+All attachments will be attached to the thread record as well as to the
+actual message.
+
+This method calls message_create that will handle management of subscription
+and notifications, and effectively create the message.
+
+If ``email_from`` is not set or ``type`` not set as 'email', a note message
+is created (comment or system notification), without the usual envelope
+attributes (sender, recipients, etc.).
mail.group
++++++++++
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index f691d75e0bc..36001ee6074 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -36,7 +36,7 @@ class mail_group(osv.osv):
A mail_group is a collection of users sharing messages in a discussion
group. Group users are users that follow the mail group, using the
subscription/follow mechanism of OpenSocial. A mail group has nothing
- in common wih res.users.group.
+ in common with res.users.group.
Additional information on fields:
- ``member_ids``: user member of the groups are calculated with
``message_get_subscribers`` method from mail.thread
@@ -49,12 +49,6 @@ class mail_group(osv.osv):
_name = 'mail.group'
_inherit = ['mail.thread']
- def action_group_join(self, cr, uid, ids, context={}):
- return self.message_subscribe(cr, uid, ids, context=context);
-
- def action_group_leave(self, cr, uid, ids, context={}):
- return self.message_unsubscribe(cr, uid, ids, context=context);
-
def onchange_photo(self, cr, uid, ids, value, context=None):
if not value:
return {'value': {'avatar_big': value, 'avatar': value} }
@@ -105,7 +99,7 @@ class mail_group(osv.osv):
message_obj = self.pool.get('mail.message')
for id in ids:
lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
- result[id] = message_obj.search(cr, uid, ['&', '&', ('model', '=', self._name), ('res_id', 'in', ids), ('date', '>=', lower_date)], count=True, context=context)
+ result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context)
return result
def _get_default_photo(self, cr, uid, context=None):
@@ -116,19 +110,33 @@ class mail_group(osv.osv):
'name': fields.char('Name', size=64, required=True),
'description': fields.text('Description'),
'responsible_id': fields.many2one('res.users', string='Responsible',
- ondelete='set null', required=True, select=1,
- help="Responsible of the group that has all rights on the record."),
- 'public': fields.boolean('Public', help='This group is visible by non members. Invisible groups can add members through the invite button.'),
- 'photo_big': fields.binary('Full-size photo', help='Field holding the full-sized PIL-supported and base64 encoded version of the group image. The photo field is used as an interface for this field.'),
- 'photo': fields.function(_get_photo, fnct_inv=_set_photo, string='Photo', type="binary",
+ ondelete='set null', required=True, select=1,
+ help="Responsible of the group that has all rights on the record."),
+ 'public': fields.boolean('Visible by non members', help='This group is visible by non members. \
+ Invisible groups can add members through the invite button.'),
+ 'group_ids': fields.many2many('res.groups', rel='mail_group_res_group_rel',
+ id1='mail_group_id', id2='groups_id', string='Linked groups',
+ help="Members of those groups will automatically added as followers. "\
+ "Note that they will be able to manage their subscription manually "\
+ "if necessary."),
+ 'photo_big': fields.binary('Full-size photo',
+ help='Field holding the full-sized PIL-supported and base64 encoded "\
+ version of the group image. The photo field is used as an "\
+ interface for this field.'),
+ 'photo': fields.function(_get_photo, fnct_inv=_set_photo,
+ string='Photo', type="binary",
store = {
'mail.group': (lambda self, cr, uid, ids, c={}: ids, ['photo_big'], 10),
- }, help='Field holding the automatically resized (128x128) PIL-supported and base64 encoded version of the group image.'),
- 'member_ids': fields.function(get_member_ids, fnct_search=search_member_ids, type='many2many',
- relation='res.users', string='Group members', multi='get_member_ids'),
- 'member_count': fields.function(get_member_ids, type='integer', string='Member count', multi='get_member_ids'),
- 'is_subscriber': fields.function(get_member_ids, type='boolean', string='Joined', multi='get_member_ids'),
- 'last_month_msg_nbr': fields.function(get_last_month_msg_nbr, type='integer', string='Messages count for last month'),
+ },
+ help='Field holding the automatically resized (128x128) PIL-supported and base64 encoded version of the group image.'),
+ 'member_ids': fields.function(get_member_ids, fnct_search=search_member_ids,
+ type='many2many', relation='res.users', string='Group members', multi='get_member_ids'),
+ 'member_count': fields.function(get_member_ids, type='integer',
+ string='Member count', multi='get_member_ids'),
+ 'is_subscriber': fields.function(get_member_ids, type='boolean',
+ string='Joined', multi='get_member_ids'),
+ 'last_month_msg_nbr': fields.function(get_last_month_msg_nbr, type='integer',
+ string='Messages count for last month'),
}
_defaults = {
@@ -136,3 +144,32 @@ class mail_group(osv.osv):
'responsible_id': (lambda s, cr, uid, ctx: uid),
'photo': _get_default_photo,
}
+
+ def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_group_ids = [command[1] for command in group_ids_command if command[0] == 4]
+ user_group_ids += [id for command in group_ids_command if command[0] == 6 for id in command[2]]
+ # retrieve the user member of those groups
+ user_ids = []
+ res_groups_obj = self.pool.get('res.groups')
+ for group in res_groups_obj.browse(cr, uid, user_group_ids, context=context):
+ user_ids += [user.id for user in group.users]
+ # subscribe the users
+ return self.message_subscribe(cr, uid, ids, user_ids, context=context)
+
+ def create(self, cr, uid, vals, context=None):
+ mail_group_id = super(mail_group, self).create(cr, uid, vals, context=context)
+ if vals.get('group_ids'):
+ self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context)
+ return mail_group_id
+
+ def write(self, cr, uid, ids, vals, context=None):
+ if vals.get('group_ids'):
+ self._subscribe_user_with_group_m2m_command(cr, uid, ids, vals.get('group_ids'), context=context)
+ return super(mail_group, self).write(cr, uid, ids, vals, context=context)
+
+ def action_group_join(self, cr, uid, ids, context=None):
+ return self.message_subscribe(cr, uid, ids, context=context)
+
+ def action_group_leave(self, cr, uid, ids, context=None):
+ return self.message_unsubscribe(cr, uid, ids, context=context)
diff --git a/addons/mail/mail_group_data.xml b/addons/mail/mail_group_data.xml
deleted file mode 100644
index c2d1762d6ae..00000000000
--- a/addons/mail/mail_group_data.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
- All Company
-
-
- Sales
-
-
-
-
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index 97210b8c179..c755209f952 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -64,6 +64,7 @@
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 301657ba4c2..b96b6e3791e 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -30,66 +30,61 @@ import datetime
from email.header import decode_header
from email.message import Message
-import tools
+from openerp import SUPERUSER_ID
from osv import osv
from osv import fields
+import pytz
+from tools import DEFAULT_SERVER_DATETIME_FORMAT
from tools.translate import _
-from openerp import SUPERUSER_ID
+import tools
_logger = logging.getLogger(__name__)
-def format_date_tz(date, tz=None):
- if not date:
- return 'n/a'
- format = tools.DEFAULT_SERVER_DATETIME_FORMAT
- return tools.server_to_local_timestamp(date, format, format, tz)
-
-def truncate_text(text):
- lines = text and text.split('\n') or []
- if len(lines) > 3:
- res = '\n\t'.join(lines[:3]) + '...'
- else:
- res = '\n\t'.join(lines)
- return res
-
+""" Some tools for parsing / creating email fields """
def decode(text):
"""Returns unicode() string conversion of the the given encoded smtp header text"""
if text:
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
-def to_email(text):
+def mail_tools_to_email(text):
"""Return a list of the email addresses found in ``text``"""
if not text: return []
return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
-class mail_message_common(osv.osv_memory):
- """Common abstract class for holding the main attributes of a
- message object. It could be reused as parent model for any
- database model or wizard screen that needs to hold a kind of
- message"""
+# TODO: remove that after cleaning
+def to_email(text):
+ return mail_tools_to_email(text)
+
+class mail_message_common(osv.TransientModel):
+ """ Common abstract class for holding the main attributes of a
+ message object. It could be reused as parent model for any
+ database model or wizard screen that needs to hold a kind of
+ message.
+ All internal logic should be in another model while this
+ model holds the basics of a message. For example, a wizard for writing
+ emails should inherit from this class and not from mail.message."""
def get_body(self, cr, uid, ids, name, arg, context=None):
- if context is None:
- context = {}
+ """ get correct body version: body_html for html messages, and
+ body_text for plain text messages
+ """
result = dict.fromkeys(ids, '')
for message in self.browse(cr, uid, ids, context=context):
- if message.subtype == 'html':
+ if message.content_subtype == 'html':
result[message.id] = message.body_html
else:
result[message.id] = message.body_text
return result
def search_body(self, cr, uid, obj, name, args, context=None):
- """will receive:
- - obj: mail.message object
- - name: 'body'
- - args: [('body', 'ilike', 'blah')]"""
- return ['|', '&', ('subtype', '=', 'html'), ('body_html', args[0][1], args[0][2]), ('body_text', args[0][1], args[0][2])]
+ # will receive:
+ # - obj: mail.message object
+ # - name: 'body'
+ # - args: [('body', 'ilike', 'blah')]
+ return ['|', '&', ('content_subtype', '=', 'html'), ('body_html', args[0][1], args[0][2]), ('body_text', args[0][1], args[0][2])]
def get_record_name(self, cr, uid, ids, name, arg, context=None):
- if context is None:
- context = {}
result = dict.fromkeys(ids, '')
for message in self.browse(cr, uid, ids, context=context):
if not message.model or not message.res_id:
@@ -116,8 +111,9 @@ class mail_message_common(osv.osv_memory):
'subject': fields.char('Subject', size=512),
'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1),
- 'record_name': fields.function(get_record_name, type='string', string='Message Record Name',
- help="Name of the record, matching the result of the name_get."),
+ 'record_name': fields.function(get_record_name, type='string',
+ string='Message Record Name',
+ help="Name get of the related document."),
'date': fields.datetime('Date'),
'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences.'),
'email_to': fields.char('To', size=256, help='Message recipients'),
@@ -125,104 +121,99 @@ class mail_message_common(osv.osv_memory):
'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
'headers': fields.text('Message Headers', readonly=1,
- help="Full message headers, e.g. SMTP session headers (usually available on inbound messages only)"),
+ help="Full message headers, e.g. SMTP session headers (usually available on inbound messages only)"),
'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
- 'subtype': fields.char('Message Type', size=32, help="Type of message, usually 'html' or 'plain', used to "
- "select plaintext or rich text contents accordingly", readonly=1),
+ 'content_subtype': fields.char('Message content subtype', size=32,
+ oldname="subtype", readonly=1,
+ help="Type of message, usually 'html' or 'plain', used to select "\
+ "plain-text or rich-text contents accordingly"),
'body_text': fields.text('Text Contents', help="Plain-text version of the message"),
'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML version of the message"),
- 'body': fields.function(get_body, fnct_search = search_body, string='Message Content', type='text',
- help="Content of the message. This content equals the body_text field for plain-test messages, and body_html for rich-text/HTML messages. This allows having one field if we want to access the content matching the message subtype."),
- 'parent_id': fields.many2one('mail.message', 'Parent Message', help="Parent message, used for displaying as threads with hierarchy",
- select=True, ondelete='set null',),
- 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
+ 'body': fields.function(get_body, fnct_search = search_body, type='text',
+ string='Message Content', store=True,
+ help="Content of the message. This content equals the body_text field "\
+ "for plain-test messages, and body_html for rich-text/HTML "\
+ "messages. This allows having one field if we want to access "\
+ "the content matching the message content_subtype."),
+ 'parent_id': fields.many2one('mail.message.common', 'Parent Message',
+ select=True, ondelete='set null',
+ help="Parent message, used for displaying as threads with hierarchy"),
}
_defaults = {
- 'subtype': 'plain',
+ 'content_subtype': 'plain',
'date': (lambda *a: fields.datetime.now()),
}
-class mail_message(osv.osv):
- '''Model holding messages: system notification (replacing res.log
- notifications), comments (for OpenSocial feature) and
+class mail_message(osv.Model):
+ """Model holding messages: system notification (replacing res.log
+ notifications), comments (for OpenChatter feature) and
RFC2822 email messages. This model also provides facilities to
parse, queue and send new email messages. Type of messages
- are differentiated using the 'type' column.
-
- The ``display_text`` field will have a slightly different
- presentation for real emails and for log messages.
- '''
+ are differentiated using the 'type' column. """
_name = 'mail.message'
_inherit = 'mail.message.common'
_description = 'Mail Message (email, comment, notification)'
_order = 'date desc'
- # XXX to review - how to determine action to use?
def open_document(self, cr, uid, ids, context=None):
+ """ Open the message related document. Note that only the document of
+ ids[0] will be opened.
+ TODO: how to determine the action to use ?
+ """
action_data = False
- if ids:
- msg = self.browse(cr, uid, ids[0], context=context)
- model = msg.model
- res_id = msg.res_id
-
- ir_act_window = self.pool.get('ir.actions.act_window')
- action_ids = ir_act_window.search(cr, uid, [('res_model', '=', model)])
- if action_ids:
- action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
- action_data.update({
- 'domain' : "[('id','=',%d)]"%(res_id),
+ if not ids:
+ return action_data
+ msg = self.browse(cr, uid, ids[0], context=context)
+ ir_act_window = self.pool.get('ir.actions.act_window')
+ action_ids = ir_act_window.search(cr, uid, [('res_model', '=', msg.model)], context=context)
+ if action_ids:
+ action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
+ action_data.update({
+ 'domain' : "[('id', '=', %d)]" % (msg.res_id),
'nodestroy': True,
'context': {}
})
return action_data
- # XXX to review - how to determine action to use?
def open_attachment(self, cr, uid, ids, context=None):
+ """ Open the message related attachments.
+ TODO: how to determine the action to use ?
+ """
action_data = False
+ if not ids:
+ return action_data
action_pool = self.pool.get('ir.actions.act_window')
- message = self.browse(cr, uid, ids, context=context)[0]
- att_ids = [x.id for x in message.attachment_ids]
- action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')])
+ messages = self.browse(cr, uid, ids, context=context)
+ att_ids = [x.id for message in messages for x in message.attachment_ids]
+ action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')], context=context)
if action_ids:
action_data = action_pool.read(cr, uid, action_ids[0], context=context)
action_data.update({
- 'domain': [('id','in',att_ids)],
+ 'domain': [('id', 'in', att_ids)],
'nodestroy': True
})
return action_data
-
- def _get_display_text(self, cr, uid, ids, name, arg, context=None):
- if context is None:
- context = {}
- tz = context.get('tz')
- result = {}
-
- # Read message as UID 1 to allow viewing author even if from different company
- for message in self.browse(cr, SUPERUSER_ID, ids):
- msg_txt = ''
- if message.email_from:
- msg_txt += _('%s wrote on %s: \n Subject: %s \n\t') % (message.email_from or '/', format_date_tz(message.date, tz), message.subject)
- if message.body_text:
- msg_txt += truncate_text(message.body_text)
- else:
- msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz) + ':\n\t'
- msg_txt += (message.subject or '')
- result[message.id] = msg_txt
- return result
_columns = {
'type': fields.selection([
('email', 'email'),
('comment', 'Comment'),
('notification', 'System notification'),
- ], 'Type', help="Message type: email for email message, notification for system message, comment for other messages such as user replies"),
- 'partner_id': fields.many2one('res.partner', 'Related partner'),
+ ], 'Type',
+ help="Message type: email for email message, notification for system "\
+ "message, comment for other messages such as user replies"),
+ 'partner_id': fields.many2one('res.partner', 'Related partner',
+ help="Deprecated field. Use partner_ids instead."),
+ 'partner_ids': fields.many2many('res.partner',
+ 'mail_message_destination_partner_rel',
+ 'message_id', 'partner_id', 'Destination partners',
+ help="When sending emails through the social network composition wizard"\
+ "you may choose to send a copy of the mail to partners."),
'user_id': fields.many2one('res.users', 'Related User', readonly=1),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
- 'display_text': fields.function(_get_display_text, method=True, type='text', size="512", string='Display Text'),
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
'state': fields.selection([
('outgoing', 'Outgoing'),
@@ -231,8 +222,14 @@ class mail_message(osv.osv):
('exception', 'Delivery Failed'),
('cancel', 'Cancelled'),
], 'Status', readonly=True),
- 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it, to save space"),
- 'original': fields.binary('Original', help="Original version of the message, as it was sent on the network", readonly=1),
+ 'auto_delete': fields.boolean('Auto Delete',
+ help="Permanently delete this email after sending it, to save space"),
+ 'original': fields.binary('Original', readonly=1,
+ help="Original version of the message, as it was sent on the network"),
+ 'parent_id': fields.many2one('mail.message', 'Parent Message',
+ select=True, ondelete='set null',
+ help="Parent message, used for displaying as threads with hierarchy"),
+ 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
}
_defaults = {
@@ -249,72 +246,124 @@ class mail_message(osv.osv):
if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
+ def check(self, cr, uid, ids, mode, context=None, values=None):
+ """Restricts the access to a mail.message, according to referred model
+ """
+ if not ids:
+ return
+ res_ids = {}
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+ cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
+ for rmod, rid in cr.fetchall():
+ if not (rmod and rid):
+ continue
+ res_ids.setdefault(rmod,set()).add(rid)
+ if values:
+ if 'res_model' in values and 'res_id' in values:
+ res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
+
+ ima_obj = self.pool.get('ir.model.access')
+ for model, mids in res_ids.items():
+ # ignore mail messages that are not attached to a resource anymore when checking access rights
+ # (resource was deleted but message was not)
+ mids = self.pool.get(model).exists(cr, uid, mids)
+ ima_obj.check(cr, uid, model, mode)
+ self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
+
+ def create(self, cr, uid, values, context=None):
+ self.check(cr, uid, [], mode='create', context=context, values=values)
+ return super(mail_message, self).create(cr, uid, values, context)
+
+ def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
+ self.check(cr, uid, ids, 'read', context=context)
+ return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
+
def copy(self, cr, uid, id, default=None, context=None):
"""Overridden to avoid duplicating fields that are unique to each email"""
if default is None:
default = {}
- default.update(message_id=False,original=False,headers=False)
+ self.check(cr, uid, [id], 'read', context=context)
+ default.update(message_id=False, original=False, headers=False)
return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
+
+ def write(self, cr, uid, ids, vals, context=None):
+ self.check(cr, uid, ids, 'write', context=context, values=vals)
+ return super(mail_message, self).write(cr, uid, ids, vals, context)
- def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None,
- email_bcc=None, reply_to=False, attachments=None, message_id=False, references=False,
- res_id=False, subtype='plain', headers=None, mail_server_id=False, auto_delete=False,
- context=None):
- """Schedule sending a new email message, to be sent the next time the mail scheduler runs, or
- the next time :meth:`process_email_queue` is called explicitly.
+ def unlink(self, cr, uid, ids, context=None):
+ self.check(cr, uid, ids, 'unlink', context=context)
+ return super(mail_message, self).unlink(cr, uid, ids, context)
- :param string email_from: sender email address
- :param list email_to: list of recipient addresses (to be joined with commas)
- :param string subject: email subject (no pre-encoding/quoting necessary)
- :param string body: email body, according to the ``subtype`` (by default, plaintext).
- If html subtype is used, the message will be automatically converted
- to plaintext and wrapped in multipart/alternative.
- :param list email_cc: optional list of string values for CC header (to be joined with commas)
- :param list email_bcc: optional list of string values for BCC header (to be joined with commas)
- :param string model: optional model name of the document this mail is related to (this will also
- be used to generate a tracking id, used to match any response related to the
- same document)
- :param int res_id: optional resource identifier this mail is related to (this will also
- be used to generate a tracking id, used to match any response related to the
- same document)
- :param string reply_to: optional value of Reply-To header
- :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
- must match the format of the ``body`` parameter. Default is 'plain',
- making the content part of the mail "text/plain".
- :param dict attachments: map of filename to filecontents, where filecontents is a string
- containing the bytes of the attachment
- :param dict headers: optional map of headers to set on the outgoing mail (may override the
- other headers, including Subject, Reply-To, Message-Id, etc.)
- :param int mail_server_id: optional id of the preferred outgoing mail server for this mail
- :param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been
- successfully sent (default to False)
+ def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email',
+ email_cc=None, email_bcc=None, reply_to=False, partner_ids=None, attachments=None,
+ message_id=False, references=False, res_id=False, content_subtype='plain',
+ headers=None, mail_server_id=False, auto_delete=False, context=None):
+ """ Schedule sending a new email message, to be sent the next time the
+ mail scheduler runs, or the next time :meth:`process_email_queue` is
+ called explicitly.
+ :param string email_from: sender email address
+ :param list email_to: list of recipient addresses (to be joined with commas)
+ :param string subject: email subject (no pre-encoding/quoting necessary)
+ :param string body: email body, according to the ``content_subtype``
+ (by default, plaintext). If html content_subtype is used, the
+ message will be automatically converted to plaintext and wrapped
+ in multipart/alternative.
+ :param list email_cc: optional list of string values for CC header
+ (to be joined with commas)
+ :param list email_bcc: optional list of string values for BCC header
+ (to be joined with commas)
+ :param string model: optional model name of the document this mail
+ is related to (this will also be used to generate a tracking id,
+ used to match any response related to the same document)
+ :param int res_id: optional resource identifier this mail is related
+ to (this will also be used to generate a tracking id, used to
+ match any response related to the same document)
+ :param string reply_to: optional value of Reply-To header
+ :param partner_ids: destination partner_ids
+ :param string content_subtype: optional mime content_subtype for
+ the text body (usually 'plain' or 'html'), must match the format
+ of the ``body`` parameter. Default is 'plain', making the content
+ part of the mail "text/plain".
+ :param dict attachments: map of filename to filecontents, where
+ filecontents is a string containing the bytes of the attachment
+ :param dict headers: optional map of headers to set on the outgoing
+ mail (may override the other headers, including Subject,
+ Reply-To, Message-Id, etc.)
+ :param int mail_server_id: optional id of the preferred outgoing
+ mail server for this mail
+ :param bool auto_delete: optional flag to turn on auto-deletion of
+ the message after it has been successfully sent (default to False)
"""
if context is None:
context = {}
if attachments is None:
attachments = {}
+ if partner_ids is None:
+ partner_ids = []
attachment_obj = self.pool.get('ir.attachment')
for param in (email_to, email_cc, email_bcc):
if param and not isinstance(param, list):
param = [param]
msg_vals = {
'subject': subject,
- 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'date': fields.datetime.now(),
'user_id': uid,
'model': model,
'res_id': res_id,
- 'type': 'email',
- 'body_text': body if subtype != 'html' else False,
- 'body_html': body if subtype == 'html' else False,
+ 'type': type,
+ 'body_text': body if content_subtype != 'html' else False,
+ 'body_html': body if content_subtype == 'html' else False,
'email_from': email_from,
'email_to': email_to and ','.join(email_to) or '',
'email_cc': email_cc and ','.join(email_cc) or '',
'email_bcc': email_bcc and ','.join(email_bcc) or '',
+ 'partner_ids': partner_ids,
'reply_to': reply_to,
'message_id': message_id,
'references': references,
- 'subtype': subtype,
+ 'content_subtype': content_subtype,
'headers': headers, # serialize the dict on the fly
'mail_server_id': mail_server_id,
'state': 'outgoing',
@@ -338,7 +387,10 @@ class mail_message(osv.osv):
return email_msg_id
def mark_outgoing(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state':'outgoing'}, 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)
def process_email_queue(self, cr, uid, ids=None, context=None):
"""Send immediately queued messages, committing after each
@@ -357,7 +409,7 @@ class mail_message(osv.osv):
if context is None:
context = {}
if not ids:
- filters = [('state', '=', 'outgoing')]
+ filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')]
if 'filters' in context:
filters.extend(context['filters'])
ids = self.search(cr, uid, filters, context=context)
@@ -371,7 +423,7 @@ class mail_message(osv.osv):
_logger.exception("Failed processing mail queue")
return res
- def parse_message(self, message, save_original=False):
+ def parse_message(self, message, save_original=False, context=None):
"""Parses a string or email.message.Message representing an
RFC-2822 email, and returns a generic dict holding the
message details.
@@ -394,7 +446,7 @@ class mail_message(osv.osv):
'headers' : { 'X-Mailer': mailer,
#.. all X- headers...
},
- 'subtype': msg_mime_subtype,
+ 'content_subtype': msg_mime_subtype,
'body_text': plaintext_body
'body_html': html_body,
'attachments': [('file1', 'bytes'),
@@ -428,49 +480,52 @@ class mail_message(osv.osv):
msg_txt['message-id'] = message_id
_logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
- fields = msg_txt.keys()
+ msg_fields = msg_txt.keys()
msg['id'] = message_id
msg['message-id'] = message_id
- if 'Subject' in fields:
+ if 'Subject' in msg_fields:
msg['subject'] = decode(msg_txt.get('Subject'))
- if 'Content-Type' in fields:
+ if 'Content-Type' in msg_fields:
msg['content-type'] = msg_txt.get('Content-Type')
- if 'From' in fields:
+ if 'From' in msg_fields:
msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
- if 'To' in fields:
+ if 'To' in msg_fields:
msg['to'] = decode(msg_txt.get('To'))
- if 'Delivered-To' in fields:
+ if 'Delivered-To' in msg_fields:
msg['to'] = decode(msg_txt.get('Delivered-To'))
- if 'CC' in fields:
+ if 'CC' in msg_fields:
msg['cc'] = decode(msg_txt.get('CC'))
- if 'Cc' in fields:
+ if 'Cc' in msg_fields:
msg['cc'] = decode(msg_txt.get('Cc'))
- if 'Reply-To' in fields:
+ if 'Reply-To' in msg_fields:
msg['reply'] = decode(msg_txt.get('Reply-To'))
- if 'Date' in fields:
+ if 'Date' in msg_fields:
date_hdr = decode(msg_txt.get('Date'))
- msg['date'] = dateutil.parser.parse(date_hdr).strftime("%Y-%m-%d %H:%M:%S")
+ # convert from email timezone to server timezone
+ date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone()))
+ date_server_datetime_str = date_server_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ msg['date'] = date_server_datetime_str
- if 'Content-Transfer-Encoding' in fields:
+ if 'Content-Transfer-Encoding' in msg_fields:
msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
- if 'References' in fields:
+ if 'References' in msg_fields:
msg['references'] = msg_txt.get('References')
- if 'In-Reply-To' in fields:
+ if 'In-Reply-To' in msg_fields:
msg['in-reply-to'] = msg_txt.get('In-Reply-To')
msg['headers'] = {}
- msg['subtype'] = 'plain'
+ msg['content_subtype'] = 'plain'
for item in msg_txt.items():
if item[0].startswith('X-'):
msg['headers'].update({item[0]: item[1]})
@@ -479,7 +534,7 @@ class mail_message(osv.osv):
body = msg_txt.get_payload(decode=True)
if 'text/html' in msg.get('content-type', ''):
msg['body_html'] = body
- msg['subtype'] = 'html'
+ msg['content_subtype'] = 'html'
if body:
body = tools.html2plaintext(body)
msg['body_text'] = tools.ustr(body, encoding)
@@ -488,9 +543,9 @@ class mail_message(osv.osv):
if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
body = ""
if 'multipart/alternative' in msg.get('content-type', ''):
- msg['subtype'] = 'alternative'
+ msg['content_subtype'] = 'alternative'
else:
- msg['subtype'] = 'mixed'
+ msg['content_subtype'] = 'mixed'
for part in msg_txt.walk():
if part.get_content_maintype() == 'multipart':
continue
@@ -504,7 +559,7 @@ class mail_message(osv.osv):
content = tools.ustr(content, encoding)
if part.get_content_subtype() == 'html':
msg['body_html'] = content
- msg['subtype'] = 'html' # html version prevails
+ msg['content_subtype'] = 'html' # html version prevails
body = tools.ustr(tools.html2plaintext(content))
body = body.replace('
', '')
elif part.get_content_subtype() == 'plain':
@@ -521,7 +576,7 @@ class mail_message(osv.osv):
# for backwards compatibility:
msg['body'] = msg['body_text']
- msg['sub_type'] = msg['subtype'] or 'plain'
+ msg['sub_type'] = msg['content_subtype'] or 'plain'
return msg
def _postprocess_sent_message(self, cr, uid, message, context=None):
@@ -535,10 +590,9 @@ class mail_message(osv.osv):
"""
if message.auto_delete:
self.pool.get('ir.attachment').unlink(cr, uid,
- [x.id for x in message.attachment_ids \
- if x.res_model == self._name and \
- x.res_id == message.id],
- context=context)
+ [x.id for x in message.attachment_ids
+ if x.res_model == self._name and x.res_id == message.id],
+ context=context)
message.unlink()
return True
@@ -557,8 +611,6 @@ class mail_message(osv.osv):
transactions (default: False)
:return: True
"""
- if context is None:
- context = {}
ir_mail_server = self.pool.get('ir.mail_server')
self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
for message in self.browse(cr, uid, ids, context=context):
@@ -567,37 +619,45 @@ class mail_message(osv.osv):
for attach in message.attachment_ids:
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
- body = message.body_html if message.subtype == 'html' else message.body_text
+ body = message.body_html if message.content_subtype == 'html' else message.body_text
body_alternative = None
- subtype_alternative = None
- if message.subtype == 'html' and message.body_text:
+ content_subtype_alternative = None
+ if message.content_subtype == 'html' and message.body_text:
# we have a plain text alternative prepared, pass it to
# build_message instead of letting it build one
body_alternative = message.body_text
- subtype_alternative = 'plain'
+ content_subtype_alternative = 'plain'
+ # handle destination_partners
+ partner_ids_email_to = ''
+ for partner in message.partner_ids:
+ partner_ids_email_to += '%s ' % (partner.email or '')
+ message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '')
+
+ # build an RFC2822 email.message.Message object and send it
+ # without queuing
msg = ir_mail_server.build_email(
email_from=message.email_from,
- email_to=to_email(message.email_to),
+ email_to=mail_tools_to_email(message_email_to),
subject=message.subject,
body=body,
body_alternative=body_alternative,
- email_cc=to_email(message.email_cc),
- email_bcc=to_email(message.email_bcc),
+ email_cc=mail_tools_to_email(message.email_cc),
+ email_bcc=mail_tools_to_email(message.email_bcc),
reply_to=message.reply_to,
attachments=attachments, message_id=message.message_id,
references = message.references,
object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
- subtype=message.subtype,
- subtype_alternative=subtype_alternative,
+ subtype=message.content_subtype,
+ subtype_alternative=content_subtype_alternative,
headers=message.headers and ast.literal_eval(message.headers))
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=message.mail_server_id.id,
context=context)
if res:
- message.write({'state':'sent', 'message_id': res})
+ message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to})
else:
- message.write({'state':'exception'})
+ message.write({'state':'exception', 'email_to': message_email_to})
message.refresh()
if message.state == 'sent':
self._postprocess_sent_message(cr, uid, message, context=context)
@@ -609,8 +669,6 @@ class mail_message(osv.osv):
cr.commit()
return True
- def cancel(self, cr, uid, ids, context=None):
- self.write(cr, uid, ids, {'state':'cancel'}, context=context)
- return True
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index 9159190ab29..eb68ab70a3b 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -30,17 +30,26 @@
+
-
+
-
+
+
+
+
+
+
+
+
+
@@ -73,6 +82,30 @@
+
+
+
+
+
+
+
+
@@ -85,55 +118,60 @@
@@ -235,13 +273,13 @@
News Feed
- mail.all_feeds
+ mail.wallMy Feeds
- mail.all_feeds
+ mail.wall
diff --git a/addons/mail/mail_subscription_view.xml b/addons/mail/mail_subscription_view.xml
index 7dbaea098dc..c3d79d48308 100644
--- a/addons/mail/mail_subscription_view.xml
+++ b/addons/mail/mail_subscription_view.xml
@@ -52,11 +52,13 @@
tree,form
-
-
+
+ -->
-
-
+
+
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index cdcaf4f464d..1655ab98afb 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -65,7 +65,7 @@ class mail_thread(osv.Model):
def _get_message_ids(self, cr, uid, ids, name, args, context=None):
res = {}
for id in ids:
- message_ids = self.message_load_ids(cr, uid, [id], context=context)
+ message_ids = self.message_search(cr, uid, [id], context=context)
subscriber_ids = self.message_get_subscribers(cr, uid, [id], context=context)
res[id] = {
'message_ids': message_ids,
@@ -130,7 +130,7 @@ class mail_thread(osv.Model):
# delete subscriptions
subscr_to_del_ids = subscr_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
subscr_obj.unlink(cr, uid, subscr_to_del_ids, context=context)
- # delete notifications
+ # delete messages and notifications
msg_to_del_ids = msg_obj.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)], context=context)
msg_obj.unlink(cr, uid, msg_to_del_ids, context=context)
@@ -150,39 +150,13 @@ class mail_thread(osv.Model):
context = {}
message_obj = self.pool.get('mail.message')
- subscription_obj = self.pool.get('mail.subscription')
notification_obj = self.pool.get('mail.notification')
- res_users_obj = self.pool.get('res.users')
- body = vals.get('body_html', '') if vals.get('subtype', 'plain') == 'html' else vals.get('body_text', '')
-
+ body = vals.get('body_html', '') if vals.get('content_subtype') == 'html' else vals.get('body_text', '')
+
# automatically subscribe the writer of the message
if vals['user_id']:
self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
-
- # get users that will get a notification pushed
- user_to_push_ids = self.message_create_get_notification_user_ids(cr, uid, [thread_id], vals, context=context)
- user_to_push_from_parse_ids = self.message_parse_users(cr, uid, [thread_id], body, context=context)
-
- # set email_from and email_to for comments and notifications
- if vals.get('type', False) and vals['type'] == 'comment' or vals['type'] == 'notification':
- current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
- if not vals.get('email_from', False):
- vals['email_from'] = current_user.user_email
- if not vals.get('email_to', False):
- email_to = ''
- for user in res_users_obj.browse(cr, uid, user_to_push_ids, context=context):
- if not user.notification_email_pref == 'all' and \
- not (user.notification_email_pref == 'comments' and vals['type'] == 'comment') and \
- not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
- continue
- if not user.user_email:
- continue
- email_to = '%s, %s' % (email_to, user.user_email)
- email_to = email_to.lstrip(', ')
- if email_to:
- vals['email_to'] = email_to
- vals['state'] = 'outgoing'
-
+
# create message
msg_id = message_obj.create(cr, uid, vals, context=context)
@@ -191,43 +165,50 @@ class mail_thread(osv.Model):
# special: if install mode, do not push demo data
if context.get('install_mode', False):
- return True
-
- # push to users
+ return msg_id
+
+ # get users that will get a notification pushed
+ user_to_push_ids = self.message_get_user_ids_to_notify(cr, uid, [thread_id], vals, context=context)
for id in user_to_push_ids:
notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
-
+
+ # create the email to send
+ email_id = self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
+
return msg_id
-
- def message_create_get_notification_user_ids(self, cr, uid, thread_ids, new_msg_vals, context=None):
- if context is None:
- context = {}
-
- notif_user_ids = []
- body = new_msg_vals.get('body_html', '') if new_msg_vals.get('subtype', 'plain') == 'html' else new_msg_vals.get('body_text', '')
- for thread_id in thread_ids:
- # add subscribers
- notif_user_ids += self.message_get_subscribers(cr, uid, [thread_id], context=context)
- # add users requested via parsing message (@login)
- notif_user_ids += self.message_parse_users(cr, uid, [thread_id], body, context=context)
- # add users requested to perform an action (need_action mechanism)
- if hasattr(self, 'get_needaction_user_ids'):
- notif_user_ids += self.get_needaction_user_ids(cr, uid, [thread_id], context=context)[thread_id]
- # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
- if new_msg_vals.get('parent_id'):
- notif_obj = self.pool.get('mail.notification')
- parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
- parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
- notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
+
+ def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
+ subscription_obj = self.pool.get('mail.subscription')
+ # get body
+ body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
+
+ # get subscribers
+ notif_user_ids = self.message_get_subscribers(cr, uid, thread_ids, context=context)
+
+ # add users requested via parsing message (@login)
+ notif_user_ids += self.message_parse_users(cr, uid, body, context=context)
+
+ # add users requested to perform an action (need_action mechanism)
+ if hasattr(self, 'get_needaction_user_ids'):
+ user_ids_dict = self.get_needaction_user_ids(cr, uid, thread_ids, context=context)
+ for id, user_ids in user_ids_dict.iteritems():
+ notif_user_ids += user_ids
+
+ # add users notified of the parent messages (because: if parent message contains @login, login must receive the replies)
+ if new_msg_vals.get('parent_id'):
+ notif_obj = self.pool.get('mail.notification')
+ parent_notif_ids = notif_obj.search(cr, uid, [('message_id', '=', new_msg_vals.get('parent_id'))], context=context)
+ parent_notifs = notif_obj.read(cr, uid, parent_notif_ids, context=context)
+ notif_user_ids += [parent_notif['user_id'][0] for parent_notif in parent_notifs]
# remove duplicate entries
notif_user_ids = list(set(notif_user_ids))
return notif_user_ids
- def message_parse_users(self, cr, uid, ids, string, context=None):
+ def message_parse_users(self, cr, uid, string, context=None):
"""Parse message content
- if find @login -(^|\s)@((\w|@|\.)*)-: returns the related ids
- this supports login that are emails (such as @admin@lapin.net)
+ this supports login that are emails (such as @raoul@grobedon.net)
"""
regex = re.compile('(^|\s)@((\w|@|\.)*)')
login_lst = [item[1] for item in regex.findall(string)]
@@ -248,46 +229,61 @@ class mail_thread(osv.Model):
return ret_dict
def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
- parent_id=False, type='email', subtype='plain', state='received',
- email_to=False, email_from=False, email_cc=None, email_bcc=None,
- reply_to=None, email_date=None, message_id=False, references=None,
- attachments=None, headers=None, original=None, context=None):
- """Creates a new mail.message attached to the current mail.thread,
- containing all the details passed as parameters. All attachments
- will be attached to the thread record as well as to the actual
- message.
- If ``email_from`` is not set or ``type`` not set as 'email',
- a note message is created, without the usual envelope
- attributes (sender, recipients, etc.).
- The creation of the message is done by calling ``message_create``
- method, that will manage automatic pushing of notifications.
+ type='email', email_date=None, parent_id=False,
+ content_subtype='plain', state=None,
+ partner_ids=None, email_from=False, email_to=False,
+ email_cc=None, email_bcc=None, reply_to=None,
+ headers=None, message_id=False, references=None,
+ attachments=None, original=None, context=None):
+ """ Creates a new mail.message through message_create. The new message
+ is attached to the current mail.thread, containing all the details
+ passed as parameters. All attachments will be attached to the
+ thread record as well as to the actual message.
+
+ This method calls message_create that will handle management of
+ subscription and notifications, and effectively create the message.
+
+ If ``email_from`` is not set or ``type`` not set as 'email',
+ a note message is created (comment or system notification),
+ without the usual envelope attributes (sender, recipients, etc.).
- :param threads: list of thread ids, or list of browse_records representing
- threads to which a new message should be attached
- :param subject: subject of the message, or description of the event if this
- is an *event log* entry.
- :param body_text: plaintext contents of the mail or log message
- :param body_html: html contents of the mail or log message
- :param parent_id: id of the parent message (threaded messaging model)
- :param type: optional type of message: 'email', 'comment', 'notification'
- :param subtype: optional subtype of message: 'plain' or 'html', corresponding to the main
- body contents (body_text or body_html).
- :param state: optional state of message; 'received' by default
- :param email_to: Email-To / Recipient address
- :param email_from: Email From / Sender address if any
- :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
- :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
- :param reply_to: reply_to header
- :param email_date: email date string if different from now, in server timezone
- :param message_id: optional email identifier
- :param references: optional email references
- :param headers: mail headers to store
- :param dict attachments: map of attachment filenames to binary contents, if any.
- :param str original: optional full source of the RFC2822 email, for reference
- :param dict context: if a ``thread_model`` value is present
- in the context, its value will be used
- to determine the model of the thread to
- update (instead of the current model).
+ :param threads: list of thread ids, or list of browse_records
+ representing threads to which a new message should be attached
+ :param subject: subject of the message, or description of the event;
+ this is totally optional as subjects are not important except
+ for specific messages (blog post, job offers) or for emails
+ :param body_text: plaintext contents of the mail or log message
+ :param body_html: html contents of the mail or log message
+ :param type: type of message: 'email', 'comment', 'notification';
+ email by default
+ :param email_date: email date string if different from now, in
+ server timezone
+ :param parent_id: id of the parent message (threaded messaging model)
+ :param content_subtype: optional content_subtype of message: 'plain'
+ or 'html', corresponding to the main body contents (body_text or
+ body_html).
+ :param state: state of message
+ :param partner_ids: destination partners of the message, in addition
+ to the now fully optional email_to; this method is supposed to
+ received a list of ids is not None. The specific many2many
+ instruction will be generated by this method.
+ :param email_from: Email From / Sender address if any
+ :param email_to: Email-To / Recipient address
+ :param email_cc: Comma-Separated list of Carbon Copy Emails To
+ addresses if any
+ :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To
+ addresses if any
+ :param reply_to: reply_to header
+ :param headers: mail headers to store
+ :param message_id: optional email identifier
+ :param references: optional email references
+ :param dict attachments: map of attachment filenames to binary
+ contents, if any.
+ :param str original: optional full source of the RFC2822 email, for
+ reference
+ :param dict context: if a ``thread_model`` value is present in the
+ context, its value will be used to determine the model of the
+ thread to update (instead of the current model).
"""
if context is None:
context = {}
@@ -323,10 +319,15 @@ class mail_thread(osv.Model):
'res_id': thread.id,
}
to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
-
+ # find related partner: partner_id column in thread object, or self is res.partner model
partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False
if not partner_id and thread._name == 'res.partner':
partner_id = thread.id
+ # destination partners
+ if partner_ids is None:
+ partner_ids = []
+ mail_partner_ids = [(6, 0, partner_ids)]
+
data = {
'subject': subject,
'body_text': body_text or (hasattr(thread, 'description') and thread.description or ''),
@@ -334,9 +335,10 @@ class mail_thread(osv.Model):
'parent_id': parent_id,
'date': email_date or fields.datetime.now(),
'type': type,
- 'subtype': subtype,
+ 'content_subtype': content_subtype,
'state': state,
'message_id': message_id,
+ 'partner_ids': mail_partner_ids,
'attachment_ids': [(6, 0, to_attach)],
'user_id': uid,
'model' : thread._name,
@@ -349,7 +351,6 @@ class mail_thread(osv.Model):
if isinstance(param, list):
param = ", ".join(param)
data.update({
- 'subject': subject or _('History'),
'email_to': email_to,
'email_from': email_from or \
(hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
@@ -385,8 +386,9 @@ class mail_thread(osv.Model):
body_html= msg_dict.get('body_html'),
parent_id = msg_dict.get('parent_id', False),
type = msg_dict.get('type', 'email'),
- subtype = msg_dict.get('subtype', 'plain'),
- state = msg_dict.get('state', 'received'),
+ content_subtype = msg_dict.get('content_subtype'),
+ state = msg_dict.get('state'),
+ partner_ids = msg_dict.get('partner_ids'),
email_from = msg_dict.get('from', msg_dict.get('email_from')),
email_to = msg_dict.get('to', msg_dict.get('email_to')),
email_cc = msg_dict.get('cc', msg_dict.get('email_cc')),
@@ -401,59 +403,109 @@ class mail_thread(osv.Model):
original = msg_dict.get('original'),
context = context)
- def _message_add_ancestor_ids(self, cr, uid, ids, child_ids, root_ids, context=None):
- """ Given message child_ids
- Find their ancestors until root ids"""
- if context is None:
- context = {}
- msg_obj = self.pool.get('mail.message')
- tmp_msgs = msg_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
- parent_ids = [msg['parent_id'][0] for msg in tmp_msgs if msg['parent_id'] and msg['parent_id'][0] not in root_ids and msg['parent_id'][0] not in child_ids]
+ #------------------------------------------------------
+ # Message loading
+ #------------------------------------------------------
+
+ def _message_search_ancestor_ids(self, cr, uid, ids, child_ids, ancestor_ids, context=None):
+ """ Given message child_ids ids, find their ancestors until ancestor_ids
+ using their parent_id relationship.
+
+ :param child_ids: the first nodes of the search
+ :param ancestor_ids: list of ancestors. When the search reach an
+ ancestor, it stops.
+ """
+ def _get_parent_ids(message_list, ancestor_ids, child_ids):
+ """ Tool function: return the list of parent_ids of messages
+ contained in message_list. Parents that are in ancestor_ids
+ or in child_ids are not returned. """
+ return [message['parent_id'][0] for message in message_list
+ if message['parent_id']
+ and message['parent_id'][0] not in ancestor_ids
+ and message['parent_id'][0] not in child_ids
+ ]
+
+ message_obj = self.pool.get('mail.message')
+ messages_temp = message_obj.read(cr, uid, child_ids, ['id', 'parent_id'], context=context)
+ parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
child_ids += parent_ids
cur_iter = 0; max_iter = 100; # avoid infinite loop
while (parent_ids and (cur_iter < max_iter)):
cur_iter += 1
- tmp_msgs = msg_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
- parent_ids = [msg['parent_id'][0] for msg in tmp_msgs if msg['parent_id'] and msg['parent_id'][0] not in root_ids and msg['parent_id'][0] not in child_ids]
+ messages_temp = message_obj.read(cr, uid, parent_ids, ['id', 'parent_id'], context=context)
+ parent_ids = _get_parent_ids(messages_temp, ancestor_ids, child_ids)
child_ids += parent_ids
if (cur_iter > max_iter):
- _logger.warning("Possible infinite loop in _message_add_ancestor_ids. Note that this algorithm is intended to check for cycle in message graph.")
+ _logger.warning("Possible infinite loop in _message_search_ancestor_ids. "\
+ "Note that this algorithm is intended to check for cycle in "\
+ "message graph, leading to a curious error. Have fun.")
return child_ids
- def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
- """ OpenChatter feature: return thread messages ids. It searches in
- mail.messages where res_id = ids, (res_)model = current model.
- :param domain: domain to add to the search; especially child_of
- is interesting when dealing with threaded display
- :param ascent: performs an ascended search; will add to fetched msgs
- all their parents until root_ids
- :param root_ids: for ascent search
- :param root_ids: root_ids when performing an ascended search
+ def message_search_get_domain(self, cr, uid, ids, context=None):
+ """ OpenChatter feature: get the domain to search the messages related
+ to a document. mail.thread defines the default behavior as
+ being messages with model = self._name, id in ids.
+ This method should be overridden if a model has to implement a
+ particular behavior.
"""
- if context is None:
- context = {}
- msg_obj = self.pool.get('mail.message')
- msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', ids), ('model', '=', self._name)] + domain,
- limit=limit, offset=offset, context=context)
- if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
- return msg_ids
+ return ['&', ('res_id', 'in', ids), ('model', '=', self._name)]
- def message_load(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[], context=None):
- """ OpenChatter feature: return thread messages
+ def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
+ limit=100, offset=0, domain=None, count=False, context=None):
+ """ OpenChatter feature: return thread messages ids according to the
+ search domain given by ``message_search_get_domain``.
+
+ It is possible to add in the search the parent of messages by
+ setting the fetch_ancestors flag to True. In that case, using
+ the parent_id relationship, the method returns the id list according
+ to the search domain, but then calls ``_message_search_ancestor_ids``
+ that will add to the list the ancestors ids. The search is limited
+ to parent messages having an id in ancestor_ids or having
+ parent_id set to False.
+
+ If ``count==True``, the number of ids is returned instead of the
+ id list. The count is done by hand instead of passing it as an
+ argument to the search call because we might want to perform
+ a research including parent messages until some ancestor_ids.
+
+ :param fetch_ancestors: performs an ascended search; will add
+ to fetched msgs all their parents until
+ ancestor_ids
+ :param ancestor_ids: used when fetching ancestors
+ :param domain: domain to add to the search; especially child_of
+ is interesting when dealing with threaded display.
+ Note that the added domain is anded with the
+ default domain.
+ :param limit, offset, count, context: as usual
"""
- msg_ids = self.message_load_ids(cr, uid, ids, limit, offset, domain, ascent, root_ids, context=context)
- msgs = self.pool.get('mail.message').read(cr, uid, msg_ids, [], context=context)
-
- # Set as read
- self.message_check_and_set_read(cr, uid, ids, context=context)
-
+ search_domain = self.message_search_get_domain(cr, uid, ids, context=context)
+ if domain:
+ search_domain += domain
+ message_obj = self.pool.get('mail.message')
+ message_res = message_obj.search(cr, uid, search_domain, limit=limit, offset=offset, count=count, context=context)
+ if not count and fetch_ancestors:
+ message_res += self._message_search_ancestor_ids(cr, uid, ids, message_res, ancestor_ids, context=context)
+ return message_res
+
+ def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
+ limit=100, offset=0, domain=None, context=None):
+ """ OpenChatter feature: read the messages related to some threads.
+ This method is used mainly the Chatter widget, to directly have
+ read result instead of searching then reading.
+
+ Please see message_search for more information about the parameters.
+ """
+ message_ids = self.message_search(cr, uid, ids, fetch_ancestors, ancestor_ids,
+ limit, offset, domain, context=context)
+ messages = self.pool.get('mail.message').read(cr, uid, message_ids, context=context)
+
""" Retrieve all attachments names """
+ map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
map_id_to_name = {}
-
- for msg in msgs:
+ for msg in messages:
for attach_id in msg["attachment_ids"]:
map_id_to_name[attach_id] = '' # use empty string as a placeholder
-
+
ids = map_id_to_name.keys()
names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
@@ -462,26 +514,34 @@ class mail_thread(osv.Model):
map_id_to_name[name[0]] = name[1]
# give corresponding ids and names to each message
- for msg in msgs:
+ for msg in messages:
msg["attachments"] = []
for attach_id in msg["attachment_ids"]:
msg["attachments"].append({'id': attach_id, 'name': map_id_to_name[attach_id]})
- """ Sort and return messages """
- msgs = sorted(msgs, key=lambda d: (-d['id']))
- return msgs
+ # Set the threads as read
+ self.message_check_and_set_read(cr, uid, ids, context=context)
+ # Sort and return the messages
+ messages = sorted(messages, key=lambda d: (-d['id']))
+ return messages
- def get_pushed_messages(self, cr, uid, ids, limit=100, offset=0, msg_search_domain=[], ascent=False, root_ids=[], context=None):
- """ OpenChatter: wall: get messages to display (=pushed notifications)
+ def message_get_pushed_messages(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
+ limit=100, offset=0, msg_search_domain=[], context=None):
+ """ OpenChatter: wall: get the pushed notifications and used them
+ to fetch messages to display on the wall.
+
+ :param fetch_ancestors: performs an ascended search; will add
+ to fetched msgs all their parents until
+ ancestor_ids
+ :param ancestor_ids: used when fetching ancestors
:param domain: domain to add to the search; especially child_of
is interesting when dealing with threaded display
:param ascent: performs an ascended search; will add to fetched msgs
all their parents until root_ids
:param root_ids: for ascent search
- :return list of mail.messages sorted by date
+ :return: list of mail.messages sorted by date
"""
- if context is None: context = {}
notification_obj = self.pool.get('mail.notification')
msg_obj = self.pool.get('mail.message')
# update message search
@@ -496,7 +556,7 @@ class mail_thread(osv.Model):
msg_ids = [notification.message_id.id for notification in notifications]
# get messages
msg_ids = msg_obj.search(cr, uid, [('id', 'in', msg_ids)], context=context)
- if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
+ if (fetch_ancestors): msg_ids = self._message_search_ancestor_ids(cr, uid, ids, msg_ids, ancestor_ids, context=context)
msgs = msg_obj.read(cr, uid, msg_ids, context=context)
return msgs
@@ -524,9 +584,9 @@ class mail_thread(osv.Model):
record needs to be created. Ignored
if the thread record already exists.
:param bool save_original: whether to keep a copy of the original
- email source attached to the message after it is imported.
+ email source attached to the message after it is imported.
:param bool strip_attachments: whether to strip all attachments
- before processing the message, in order to save some space.
+ before processing the message, in order to save some space.
"""
# extract message bytes - we are forced to pass the message as binary because
# we don't know its encoding until we parse its headers and hence can't
@@ -540,7 +600,6 @@ class mail_thread(osv.Model):
context.update({'thread_model': model})
mail_message = self.pool.get('mail.message')
- res_id = False
# Parse Message
# Warning: message_from_string doesn't always work correctly on unicode,
@@ -548,8 +607,11 @@ class mail_thread(osv.Model):
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
- msg = mail_message.parse_message(msg_txt, save_original=save_original)
+ msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context)
+ # update state
+ msg['state'] = 'received'
+
if strip_attachments and 'attachments' in msg:
del msg['attachments']
@@ -587,6 +649,8 @@ class mail_thread(osv.Model):
res_id = create_record(msg)
# To forward the email to other followers
self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
+ # Set as Unread
+ model_pool.message_mark_as_unread(cr, uid, [res_id], context=context)
return res_id
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
@@ -652,9 +716,9 @@ class mail_thread(osv.Model):
return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
def message_thread_followers(self, cr, uid, ids, context=None):
- """Returns a list of email addresses of the people following
- this thread, including the sender of each mail, and the
- people who were in CC of the messages, if any.
+ """ Returns a list of email addresses of the people following
+ this thread, including the sender of each mail, and the
+ people who were in CC of the messages, if any.
"""
res = {}
if isinstance(ids, (str, int, long)):
@@ -748,45 +812,25 @@ class mail_thread(osv.Model):
# Note specific
#------------------------------------------------------
- def message_broadcast(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
- if context is None:
- context = {}
- notification_obj = self.pool.get('mail.notification')
- # write message
- msg_ids = self.message_append_note(cr, uid, ids, subject=subject, body=body, parent_id=parent_id, type=type, subtype=subtype, context=context)
- # escape if in install mode or note writing was not successfull
- if 'install_mode' in context:
- return True
- if not isinstance(msg_ids, (list)):
- return True
- # get already existing notigications
- notification_ids = notification_obj.search(cr, uid, [('message_id', 'in', msg_ids)], context=context)
- already_pushed_user_ids = map(itemgetter('user_id'), notification_obj.read(cr, uid, notification_ids, context=context))
- # get base.group_user group
- res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user') or False
- group_id = res and res[1] or False
- if not group_id: return True
- group = self.pool.get('res.groups').browse(cr, uid, [group_id], context=context)[0]
- for user in group.users:
- if user.id in already_pushed_user_ids: continue
- for msg_id in msg_ids:
- notification_obj.create(cr, uid, {'user_id': user.id, 'message_id': msg_id}, context=context)
- return True
-
def log(self, cr, uid, id, message, secondary=False, context=None):
- _logger.warning("log() is deprecated. Please use OpenChatter notification system instead of the res.log mechanism.")
+ _logger.warning("log() is deprecated. As this module inherit from \
+ mail.thread, the message will be managed by this \
+ module instead of by the res.log mechanism. Please \
+ use the mail.thread OpenChatter API instead of the \
+ now deprecated res.log.")
self.message_append_note(cr, uid, [id], 'res.log', message, context=context)
- def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False, type='notification', subtype='html', context=None):
- if type in ['notification', 'reply']:
- subject = None
- if subtype == 'html':
+ def message_append_note(self, cr, uid, ids, subject=None, body=None, parent_id=False,
+ type='notification', content_subtype='html', context=None):
+ if content_subtype == 'html':
body_html = body
body_text = body
else:
body_html = body
body_text = body
- return self.message_append(cr, uid, ids, subject, body_html=body_html, body_text=body_text, parent_id=parent_id, type=type, subtype=subtype, context=context)
+ return self.message_append(cr, uid, ids, subject, body_html, body_text,
+ type, parent_id=parent_id,
+ content_subtype=content_subtype, context=context)
#------------------------------------------------------
# Subscription mechanism
@@ -796,9 +840,9 @@ class mail_thread(osv.Model):
""" Returns the current document followers. Basically this method
checks in mail.subscription for entries with matching res_model,
res_id.
-
- :param get_ids: if set to True, return the ids of users; if set
- to False, returns the result of a read in res.users
+ This method can be overriden to add implicit subscribers, such
+ as project managers, by adding their user_id to the list of
+ ids returned by this method.
"""
subscr_obj = self.pool.get('mail.subscription')
subscr_ids = subscr_obj.search(cr, uid, ['&', ('res_model', '=', self._name), ('res_id', 'in', ids)], context=context)
@@ -849,19 +893,97 @@ class mail_thread(osv.Model):
# Trying to unsubscribe somebody not in subscribers: returns False
# if special management is needed; allows to know that an automatically
# subscribed user tries to unsubscribe and allows to warn him
- mail_thread_model = self.pool.get('mail.thread')
- if not user_ids and not uid in mail_thread_model.message_get_subscribers(cr, uid, ids, context=context):
- return False
- subscription_obj = self.pool.get('mail.subscription')
to_unsubscribe_uids = [uid] if user_ids is None else user_ids
+ subscription_obj = self.pool.get('mail.subscription')
to_delete_sub_ids = subscription_obj.search(cr, uid,
['&', '&', ('res_model', '=', self._name), ('res_id', 'in', ids), ('user_id', 'in', to_unsubscribe_uids)], context=context)
+ if not to_delete_sub_ids:
+ return False
return subscription_obj.unlink(cr, uid, to_delete_sub_ids, context=context)
#------------------------------------------------------
# Notification API
#------------------------------------------------------
+ def message_create_notify_by_email(self, cr, uid, new_msg_values, user_to_notify_ids, context=None):
+ """ When creating a new message and pushing notifications, emails
+ must be send if users have chosen to receive notifications
+ by email via the notification_email_pref field.
+
+ ``notification_email_pref`` can have 3 values :
+ - all: receive all notification by email (for example for shared
+ users)
+ - to_me: messages send directly to me (@login, messages on res.users)
+ - never: never receive notifications
+ Note that an user should never receive notifications for messages
+ he has created.
+
+ :param new_msg_values: dictionary of message values, those that
+ are given to the create method
+ :param user_to_notify_ids: list of user_ids, user that will
+ receive a notification on their Wall
+ """
+ message_obj = self.pool.get('mail.message')
+ res_users_obj = self.pool.get('res.users')
+ body = new_msg_values.get('body_html', '') if new_msg_values.get('content_subtype') == 'html' else new_msg_values.get('body_text', '')
+
+ # remove message writer
+ if user_to_notify_ids.count(new_msg_values.get('user_id')) > 0:
+ user_to_notify_ids.remove(new_msg_values.get('user_id'))
+
+ # get user_ids directly asked
+ user_to_push_from_parse_ids = self.message_parse_users(cr, uid, body, context=context)
+
+ # try to find an email_to
+ email_to = ''
+ for user in res_users_obj.browse(cr, uid, user_to_notify_ids, context=context):
+ if not user.notification_email_pref == 'all' and \
+ not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
+ continue
+ if not user.user_email:
+ continue
+ email_to = '%s, %s' % (email_to, user.user_email)
+ email_to = email_to.lstrip(', ')
+
+ # did not find any email address: not necessary to create an email
+ if not email_to:
+ return
+
+ # try to find an email_from
+ current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
+ email_from = new_msg_values.get('email_from')
+ if not email_from:
+ email_from = current_user.user_email
+
+ # get email content, create it (with mail_message.create)
+ email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
+ email_id = message_obj.create(cr, uid, email_values, context=context)
+ return email_id
+
+ def message_create_notify_get_email_dict(self, cr, uid, new_msg_values, email_from, email_to, context=None):
+ values = dict(new_msg_values)
+
+ body_html = new_msg_values.get('body_html', '')
+ if body_html:
+ body_html += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
+ body_text = new_msg_values.get('body_text', '')
+ if body_text:
+ body_text += '\n\n----------\nThis email was send automatically by OpenERP, because you have subscribed to a document.'
+ values.update({
+ 'type': 'email',
+ 'state': 'outgoing',
+ 'email_from': email_from,
+ 'email_to': email_to,
+ 'subject': 'New message',
+ 'content_subtype': new_msg_values.get('content_subtype', 'plain'),
+ 'body_html': body_html,
+ 'body_text': body_text,
+ 'auto_delete': True,
+ 'res_model': '',
+ 'res_id': False,
+ })
+ return values
+
def message_remove_pushed_notifications(self, cr, uid, ids, msg_ids, remove_childs=True, context=None):
notif_obj = self.pool.get('mail.notification')
msg_obj = self.pool.get('mail.message')
diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml
index a32045efafc..f07e1e467ee 100644
--- a/addons/mail/mail_thread_view.xml
+++ b/addons/mail/mail_thread_view.xml
@@ -2,11 +2,11 @@
-
-
+
+
@@ -22,5 +22,6 @@
+
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index 922f3e4b6ed..3dc1f91d857 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -22,34 +22,18 @@
from osv import osv
from osv import fields
-class res_partner(osv.osv):
+class res_partner_mail(osv.osv):
""" Inherits partner and adds CRM information in the partner form """
_name = "res.partner"
_inherit = ['res.partner', 'mail.thread']
- _columns = {
- 'emails': fields.one2many('mail.message', 'partner_id', 'Emails', readonly=True, domain=[('email_from','!=',False)]),
- }
- def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[False], context=None):
- """ Override of message_load_ids
- partner discussion page :
- - messages posted on res.partner, partner_id = partner.id
- - messages directly sent to partner
+ def message_search_get_domain(self, cr, uid, ids, context=None):
+ """ Override of message_search_get_domain for partner discussion page.
+ The purpose is to add messages directly sent to the partner.
"""
- msg_obj = self.pool.get('mail.message')
- msg_ids = []
- partner_ids=[]
- for partner in self.browse(cr, uid, ids, context=context):
- msg_ids += msg_obj.search(cr, uid, [ ('res_id', '=', partner.id), ('model', '=' ,self._name)] + domain,
- limit=limit, offset=offset, context=context)
- if self._name=='res.partner':
- partner_ids=msg_obj.search(cr, uid, [ ('partner_id', 'in', ids)] + domain,
- limit=limit, offset=offset, context=context)
- if partner_ids :
- msg_ids+= partner_ids
- if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
- return msg_ids
-
-res_partner()
+ initial_domain = super(res_partner_mail, self).message_search_get_domain(cr, uid, ids, context=context)
+ if self._name == 'res.partner': # to avoid models inheriting from res.partner
+ search_domain = ['|'] + initial_domain + ['|', ('partner_id', 'in', ids), ('partner_ids', 'in', ids)]
+ return search_domain
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
index 2d425f18c7b..d1e2d881739 100644
--- a/addons/mail/res_partner_view.xml
+++ b/addons/mail/res_partner_view.xml
@@ -9,8 +9,8 @@
-
-
+
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index f62e29fb44d..89767473754 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -24,24 +24,26 @@ from tools.translate import _
class res_users(osv.osv):
""" Update of res.users class
- - add a preference about sending emails about notificatoins
+ - add a preference about sending emails about notifications
- make a new user follow itself
+ - add a welcome message
"""
_name = 'res.users'
_inherit = ['res.users', 'mail.thread']
_columns = {
'notification_email_pref': fields.selection([
- ('all', 'All feeds'),
- ('comments', 'Only comments'),
- ('to_me', 'Only when sent directly to me'),
- ('none', 'Never')
- ], 'Receive Feeds by Email', required=True,
- help="Choose in which case you want to receive an email when you receive new feeds."),
+ ('all', 'All feeds'),
+ ('comments', 'Only comments'),
+ ('to_me', 'Only when sent directly to me'),
+ ('none', 'Never')
+ ], 'Receive Feeds by Email', required=True,
+ help="Choose in which case you want to receive an email when you "\
+ "receive new feeds."),
}
_defaults = {
- 'notification_email_pref': 'none',
+ 'notification_email_pref': 'to_me',
}
def __init__(self, pool, cr):
@@ -60,25 +62,61 @@ class res_users(osv.osv):
user = self.browse(cr, uid, [user_id], context=context)[0]
# make user follow itself
self.message_subscribe(cr, uid, [user_id], [user_id], context=context)
- # create a welcome message to broadcast
+ # create a welcome message
company_name = user.company_id.name if user.company_id else 'the company'
- message = _('%s has joined %s! You may leave him/her a message to celebrate a new arrival in the company ! You can help him/her doing its first steps on OpenERP.') % (user.name, company_name)
- # TODO: clean the broadcast feature. As this is not cleany specified, temporarily remove the message broadcasting that is not buggy but not very nice.
- #self.message_broadcast(cr, uid, [user.id], 'Welcome notification', message, context=context)
+ message = _('%s has joined %s! Welcome in OpenERP !') % (user.name, company_name)
+ self.message_append_note(cr, uid, [user_id], subject='Welcom to OpenERP', body=message, type='comment', context=context)
return user_id
- def message_load_ids(self, cr, uid, ids, limit=100, offset=0, domain=[], ascent=False, root_ids=[False], context=None):
- """ Override of message_load_ids
- User discussion page :
- - messages posted on res.users, res_id = user.id
- - messages directly sent to user with @user_login
+ def message_search_get_domain(self, cr, uid, ids, context=None):
+ """ Override of message_search_get_domain for partner discussion page.
+ The purpose is to add messages directly sent to user using
+ @user_login.
"""
- if context is None:
- context = {}
- msg_obj = self.pool.get('mail.message')
- msg_ids = []
+ initial_domain = super(res_users, self).message_search_get_domain(cr, uid, ids, context=context)
+ custom_domain = []
for user in self.browse(cr, uid, ids, context=context):
- msg_ids += msg_obj.search(cr, uid, ['|', '|', ('body_text', 'like', '@%s' % (user.login)), ('body_html', 'like', '@%s' % (user.login)), '&', ('res_id', '=', user.id), ('model', '=', self._name)] + domain,
- limit=limit, offset=offset, context=context)
- if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
- return msg_ids
+ if custom_domain:
+ custom_domain += ['|']
+ custom_domain += ['|', ('body_text', 'like', '@%s' % (user.login)), ('body_html', 'like', '@%s' % (user.login))]
+ return ['|'] + initial_domain + custom_domain
+
+class res_users_mail_group(osv.osv):
+ """ Update of res.groups class
+ - if adding/removing users from a group, check mail.groups linked to
+ this user group, and subscribe / unsubscribe them from the discussion
+ group. This is done by overriding the write method.
+ """
+ _name = 'res.users'
+ _inherit = ['res.users', 'mail.thread']
+
+ def write(self, cr, uid, ids, vals, context=None):
+ write_res = super(res_users_mail_group, self).write(cr, uid, ids, vals, context=context)
+ if vals.get('groups_id'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4]
+ user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]]
+ mail_group_obj = self.pool.get('mail.group')
+ mail_group_ids = mail_group_obj.search(cr, uid, [('group_ids', 'in', user_group_ids)], context=context)
+ mail_group_obj.message_subscribe(cr, uid, mail_group_ids, ids, context=context)
+ return write_res
+
+
+class res_groups_mail_group(osv.osv):
+ """ Update of res.groups class
+ - if adding/removing users from a group, check mail.groups linked to
+ this user group, and subscribe / unsubscribe them from the discussion
+ group. This is done by overriding the write method.
+ """
+ _name = 'res.groups'
+ _inherit = 'res.groups'
+
+ def write(self, cr, uid, ids, vals, context=None):
+ if vals.get('users'):
+ # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
+ user_ids = [command[1] for command in vals['users'] if command[0] == 4]
+ user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]]
+ mail_group_obj = self.pool.get('mail.group')
+ mail_group_ids = mail_group_obj.search(cr, uid, [('group_ids', 'in', ids)], context=context)
+ mail_group_obj.message_subscribe(cr, uid, mail_group_ids, user_ids, context=context)
+ return super(res_groups_mail_group, self).write(cr, uid, ids, vals, context=context)
diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml
index 918f77e0ace..e268997ac2c 100644
--- a/addons/mail/res_users_view.xml
+++ b/addons/mail/res_users_view.xml
@@ -9,7 +9,7 @@
-
+
@@ -23,11 +23,11 @@
-
+
-
+
diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv
index 011e8b27dc5..1fb909f4435 100644
--- a/addons/mail/security/ir.model.access.csv
+++ b/addons/mail/security/ir.model.access.csv
@@ -1,7 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_mail_message,mail.message,model_mail_message,,1,0,1,0
-access_mail_message_all,mail.message.all,model_mail_message,base.group_user,1,1,1,1
+access_mail_message_all,mail.message.all,model_mail_message,,1,0,0,0
+access_mail_message_group_user,mail.message.group.user,model_mail_message,base.group_user,1,1,1,1
access_mail_thread,mail.thread,model_mail_thread,base.group_user,1,1,1,0
-access_mail_subscription,mail.subscription,model_mail_subscription,,1,1,1,1
-access_mail_notification,mail.notification,model_mail_notification,,1,1,1,1
+access_mail_subscription_all,mail.subscription.all,model_mail_subscription,,1,1,1,1
+access_mail_notification_all,mail.notification.all,model_mail_notification,,1,1,1,1
access_mail_group,mail.group,model_mail_group,base.group_user,1,1,1,1
diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css
index 0cf932f9a25..03f1078e6d5 100644
--- a/addons/mail/static/src/css/mail.css
+++ b/addons/mail/static/src/css/mail.css
@@ -1,7 +1,6 @@
-
-/* ------------------------------ */
-/* Wall */
-/* ------------------------------ */
+/* ------------------------------------------------------------ */
+/* Wall
+/* ------------------------------------------------------------ */
.openerp div.oe_mail_wall {
overflow: auto;
@@ -9,6 +8,17 @@
background: white;
}
+.openerp div.oe_mail_wall_main {
+ float: left;
+ width: 560px;
+ margin: 8px;
+}
+
+.openerp div.oe_mail_wall_aside {
+ margin-left: 565px;
+ margin: 8px;
+}
+
.openerp div.oe_mail_wall_action {
padding: 8px;
background: #eee;
@@ -21,36 +31,32 @@
clear: both;
}
-.openerp .oe_mail_wall_action textarea {
+.openerp div.oe_mail_wall_action .oe_mail_msg_content {
+ width: 484px;
+}
+
+.openerp div.oe_mail_wall_action textarea.oe_mail_compose_textarea,
+.openerp div.oe_mail_wall_action div.oe_mail_compose_message_body_text textarea {
width: 474px;
height: 60px;
padding: 4px;
- margin-bottom: 8px;
- float: right;
+ margin-bottom: 2px;
}
-/* 2 columns view */
-.openerp div.oe_mail_wall_main {
- float: left;
- width: 560px;
- margin: 8px;
-}
-
-.openerp div.oe_mail_wall_aside {
- margin-left: 565px;
- margin: 8px;
-}
-
-/* Threads */
-.openerp .oe_mail_wall_threads {
+.openerp ul.oe_mail_wall_threads {
margin-top: 8px;
}
-.openerp .oe_mail_wall_threads textarea {
- height: 40px;
+
+/* Specific display of threads in the wall */
+/* ------------------------------------------------------------ */
+
+.openerp ul.oe_mail_wall_threads .oe_mail_msg_content textarea {
+ width: 434px;
+ height: 30px;
padding: 4px;
}
-.openerp .oe_mail_wall_thread:first .oe_mail_msg_notification {
+.openerp li.oe_mail_wall_thread:first .oe_mail_msg_notification {
border-top: 0;
}
@@ -64,7 +70,7 @@
width: 486px;
}
-.openerp div.oe_mail_msg_content li{
+.openerp div.oe_mail_msg_content li {
float: left;
margin-right: 3px;
}
@@ -79,40 +85,28 @@
}
-/* ------------------------------ */
-/* RecordThread */
-/* ------------------------------ */
+/* ------------------------------------------------------------ */
+/* RecordThread
+/* ------------------------------------------------------------ */
.openerp div.oe_mail_recthread {
overflow: auto;
}
-/* Left-side CSS */
-.openerp div.oe_mail_recthread_actions {
- margin-bottom: 8px;
-}
-
-.openerp div.oe_mail_recthread_followers {
- margin-bottom: 8px;
-}
-
-.openerp div.oe_mail_recthread_followers img.oe_mail_msg_image {
- width: 28px;
- height: 28px;
- margin: 4px;
-}
-
-/* RecordThread: 2 columns view */
-.openerp div.oe_mail_recthread_left {
+.openerp div.oe_mail_recthread_main {
float: left;
width: 560px;
}
-.openerp div.oe_mail_recthread_right {
+.openerp div.oe_mail_recthread_aside {
float: right;
width: 250px;
}
+.openerp div.oe_mail_recthread_actions {
+ margin-bottom: 8px;
+}
+
.openerp div.oe_mail_recthread_actions button {
width: 120px;
}
@@ -144,16 +138,17 @@
background-image: linear-gradient(to bottom, #dc5f59, #b33630);
}
-.openerp textarea.oe_mail_action_textarea {
- height: 60px;
- padding: 5px;
+.openerp div.oe_mail_recthread_followers {
+ margin-bottom: 8px;
}
-/* ------------------------------ */
-/* ThreadDisplay */
-/* ------------------------------ */
+
+/* ------------------------------------------------------------ */
+/* Thread
+/* ------------------------------------------------------------ */
.openerp div.oe_mail_thread_action {
+ display: none;
white-space: normal;
padding: 8px;
background: #eee;
@@ -166,6 +161,26 @@
clear: both;
}
+/* default textarea (oe_mail_compose_textarea), and body_text textarea for compose form view */
+.openerp .oe_mail_msg_content textarea.oe_mail_compose_textarea,
+.openerp .oe_mail_msg_content div.oe_mail_compose_message_body_text textarea {
+ width: 474px;
+ height: 60px;
+ padding: 4px;
+ font-size: 12px;
+ border: 1px solid #cccccc;
+}
+
+/* default textarea (oe_mail_compose_textarea), and body_text textarea for compose form view */
+.openerp .oe_mail_msg_content textarea.oe_mail_compose_textarea:focus,
+.openerp .oe_mail_msg_content div.oe_mail_compose_message_body_text textarea:focus {
+ outline: 0;
+ border-color: rgba(82, 168, 236, 0.8);
+ -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ -box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+}
+
.openerp div.oe_mail_thread_display {
white-space: normal;
}
@@ -174,7 +189,7 @@
margin-left: 66px;
}
-.openerp div.oe_mail_thread_subthread .oe_mail_thread_msg:last-child {
+.openerp div.oe_mail_thread_subthread li.oe_mail_thread_msg:last-child {
margin-bottom: 8px;
}
@@ -183,18 +198,23 @@
border-bottom: 1px solid #D2D9E7;
}
-.openerp .oe_mail_thread_msg:after {
+.openerp li.oe_mail_thread_msg:after {
content: "";
display: block;
clear: both;
}
-.openerp .oe_mail_thread_msg > div:after {
+.openerp li.oe_mail_thread_msg > div:after {
content: "";
display: block;
clear: both;
}
+.openerp div.oe_mail_msg {
+ padding: 0;
+ margin: 0 0 4px 0;
+}
+
.openerp .oe_mail_msg_notification,
.openerp .oe_mail_msg_comment,
.openerp .oe_mail_msg_email {
@@ -203,11 +223,6 @@
border-top: 1px solid #ccc;
}
-.openerp .oe_email_icon {
- width: 50px;
- height: 50px;
-}
-
.openerp div.oe_mail_thread_subthread .oe_mail_msg_comment {
background: #eee;
}
@@ -230,9 +245,18 @@
clear: both;
}
-.openerp img.oe_mail_msg_image {
+.openerp img.oe_mail_icon {
width: 50px;
height: 50px;
+}
+
+.openerp img.oe_mail_thumbnail {
+ width: 28px;
+ height: 28px;
+ margin: 4px;
+}
+
+.openerp img.oe_mail_frame {
text-align: center;
overflow: hidden;
-moz-border-radius: 3px;
@@ -247,48 +271,60 @@
clip: rect(5px, 40px, 45px, 0px);
}
-/* ------------------------------ */
-/* Styling (should be openerp) */
-/* ------------------------------ */
+.openerp .oe_mail_invisible {
+ display: none;
+}
-.openerp input.oe_mail, textarea.oe_mail {
- width: 432px;
- padding: 4px;
- font-size: 12px;
+/* ------------------------------------------------------------ */
+/* mail.compose.message form view & OpenERP hacks
+/* ------------------------------------------------------------ */
+
+/* form_view: delete white background */
+.openerp .oe_mail_msg_content div.oe_formview {
+ background-color: transparent;
+}
+
+.openerp .oe_mail_msg_content div.oe_form_nosheet {
+ margin: 0px;
+}
+
+.openerp .oe_mail_msg_content table.oe_form_group {
+ margin: 0px;
+}
+
+.openerp .oe_mail_msg_content table.oe_form_field,
+.openerp .oe_mail_msg_content div.oe_form_field {
+ padding: 0px;
+}
+
+.openerp .oe_mail_msg_content td.oe_form_group_cell {
+ vertical-align: bottom;
+}
+
+/* subject: change width */
+.openerp .oe_mail_msg_content .oe_form .oe_form_field input {
+ width: 472px;
+}
+
+/* body_html: cleditor */
+.openerp .oe_mail_msg_content div.cleditorMain {
border: 1px solid #cccccc;
- -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
- -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
- -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
- -o-transition: border linear 0.2s, box-shadow linear 0.2s;
- transition: border linear 0.2s, box-shadow linear 0.2s;
- -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
- -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
- -box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
}
-.openerp input.oe_mail:focus, textarea.oe_mail:focus {
- outline: 0;
- border-color: rgba(82, 168, 236, 0.8);
- -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
- -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
- -box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+/* destination_partner_ids */
+.openerp .oe_mail_msg_content div.text-core {
+ height: 22px !important;
+ width: 472px;
}
-.openerp div.oe_mail_msg {
- padding: 0;
- margin: 0 0 4px 0;
+/* buttons */
+.openerp .oe_mail_msg_content .oe_mail_compose_message_icons button.oe_form_button {
+ padding: 1px;
}
-.openerp .oe_mail_oe_bold {
- font-weight: bold;
-}
-
-/* ------------------------------ */
-/* Messages layout */
-/* ------------------------------ */
+/* ------------------------------------------------------------ */
+/* Messages layout
+/* ------------------------------------------------------------ */
.openerp .oe_mail_msg .oe_mail_msg_title {
margin: 0;
@@ -318,6 +354,10 @@
display: inline;
}
+.openerp .oe_mail_oe_bold {
+ font-weight: bold;
+}
+
/* Read more/less link */
.openerp .oe_mail_msg_content .expand,
.openerp .oe_mail_msg_content .reduce {
@@ -360,35 +400,3 @@
padding: 0;
list-style-type: square;
}
-
-
-/* ------------------------------ */
-/* Group Form */
-/* ------------------------------ */
-
-div.oe_mail_group_advanced_details {
- display: none;
-}
-
-.oe_form_sheetbg.openerp_mail_group_sheet {
- min-height: 0px;
- max-height: none;
-}
-
-.oe_form_sheetbg.openerp_mail_group_sheet .oe_form_sheet {
- min-height: 0px;
- max-height: none;
- padding: 0px 18px;
- max-width: 80%;
-}
-
-/* Resize group logo */
-.oe_form_sheetbg.openerp_mail_group_sheet .oe_form_field_image > img {
- max-width: 100px;
- max-height: 100px;
-}
-
-/* Resize group description */
-.oe_form_sheetbg.openerp_mail_group_sheet .oe_form_field_text > textarea {
- height: 40px;
-}
diff --git a/addons/mail/static/src/css/mail_compose_message.css b/addons/mail/static/src/css/mail_compose_message.css
new file mode 100644
index 00000000000..4cd1339ded4
--- /dev/null
+++ b/addons/mail/static/src/css/mail_compose_message.css
@@ -0,0 +1,16 @@
+/* ------------------------------ */
+/* Compose Message Wizard Form */
+/* ------------------------------ */
+
+.openerp tr td .oe_form_field.oe_mail_compose_message_invisible {
+ display: none;
+}
+
+.openerp .oe_mail_compose_message_icons {
+ text-align: right;
+}
+
+.openerp .oe_mail_compose_message_icons img {
+ width: 20px;
+ height: 20px;
+}
diff --git a/addons/mail/static/src/img/_fp.png b/addons/mail/static/src/img/_fp.png
deleted file mode 100644
index 17dce2a047c..00000000000
Binary files a/addons/mail/static/src/img/_fp.png and /dev/null differ
diff --git a/addons/mail/static/src/img/attachment.png b/addons/mail/static/src/img/attachment.png
new file mode 100644
index 00000000000..5cc0c33257d
Binary files /dev/null and b/addons/mail/static/src/img/attachment.png differ
diff --git a/addons/mail/static/src/img/checklist.png b/addons/mail/static/src/img/checklist.png
new file mode 100644
index 00000000000..d252606ff53
Binary files /dev/null and b/addons/mail/static/src/img/checklist.png differ
diff --git a/addons/mail/static/src/img/feeds-hover.png b/addons/mail/static/src/img/feeds-hover.png
deleted file mode 100644
index 67070d3e7df..00000000000
Binary files a/addons/mail/static/src/img/feeds-hover.png and /dev/null differ
diff --git a/addons/mail/static/src/img/feeds.png b/addons/mail/static/src/img/feeds.png
deleted file mode 100644
index ac2293c0bf5..00000000000
Binary files a/addons/mail/static/src/img/feeds.png and /dev/null differ
diff --git a/addons/mail/static/src/img/formatting.png b/addons/mail/static/src/img/formatting.png
new file mode 100644
index 00000000000..cf45fdf192f
Binary files /dev/null and b/addons/mail/static/src/img/formatting.png differ
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index 2140bbe474a..8ce050fd144 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -5,210 +5,652 @@ openerp.mail = function(session) {
var mail = session.mail = {};
/**
- * Add records to sorted_comments array
- * @param {Array} records records from mail.message sorted by date desc
- * @returns {Object} cs comments_structure: dict
- * cs.model_to_root_ids = {model: [root_ids], }
- * cs.new_root_ids = [new_root_ids]
- * cs.root_ids = [root_ids]
- * cs.msgs = {record.id: record,}
- * cs.tree_struct = {record.id: {
- * 'level': record_level in hierarchy, 0 is root,
- * 'msg_nbr': number of childs,
- * 'direct_childs': [msg_ids],
- * 'all_childs': [msg_ids],
- * 'for_thread_msgs': [records],
- * 'ancestors': [msg_ids], } }
+ * ------------------------------------------------------------
+ * FormView
+ * ------------------------------------------------------------
+ *
+ * Override of formview do_action method, to catch all return action about
+ * mail.compose.message. The purpose is to bind 'Send by e-mail' buttons
+ * and redirect them to the Chatter.
*/
- function tools_sort_comments(cs, records, parent_id) {
- var cur_iter = 0; var max_iter = 10; var modif = true;
- while ( modif && (cur_iter++) < max_iter) {
- modif = false;
- _(records).each(function (record) {
- // root and not yet recorded
- if ( (record.parent_id == false || record.parent_id[0] == parent_id) && ! cs['msgs'][record.id]) {
- // add to model -> root_list ids
- if (! cs['model_to_root_ids'][record.model]) cs['model_to_root_ids'][record.model] = [record.id];
- else cs['model_to_root_ids'][record.model].push(record.id);
- // add root data
- cs['new_root_ids'].push(record.id);
- // add record
- cs['tree_struct'][record.id] = {'level': 0, 'direct_childs': [], 'all_childs': [], 'for_thread_msgs': [record], 'msg_nbr': -1, 'ancestors': []};
- cs['msgs'][record.id] = record;
- modif = true;
- }
- // not yet recorded, but parent is recorded
- else if (! cs['msgs'][record.id] && cs['msgs'][record.parent_id[0]]) {
- var parent_level = cs['tree_struct'][record.parent_id[0]]['level'];
- // update parent structure
- cs['tree_struct'][record.parent_id[0]]['direct_childs'].push(record.id);
- cs['tree_struct'][record.parent_id[0]]['for_thread_msgs'].push(record);
- // update ancestors structure
- for (ancestor_id in cs['tree_struct'][record.parent_id[0]]['ancestors']) {
- cs['tree_struct'][ancestor_id]['all_childs'].push(record.id);
+
+ session.web.FormView = session.web.FormView.extend({
+ do_action: function(action, on_close) {
+ if (action.res_model == 'mail.compose.message' && this.fields && this.fields.message_ids) {
+ var record_thread = this.fields.message_ids;
+ var thread = record_thread.thread;
+ thread.instantiate_composition_form('comment', true, false, 0, action.context);
+ return false;
+ }
+ else {
+ return this._super(action, on_close);
+ }
+ },
+ });
+
+
+ /**
+ * ------------------------------------------------------------
+ * ChatterUtils
+ * ------------------------------------------------------------
+ *
+ * This class holds a few tools method that will be used by
+ * the various Chatter widgets.
+ */
+
+ mail.ChatterUtils = {
+
+ /**
+ * mail_int_mapping: structure to keep a trace of internal links mapping
+ * mail_int_mapping['model'] = {
+ * 'name_get': [[id,label], [id,label], ...]
+ * 'fetch_ids': [id, id, ...] } */
+ //var mail_int_mapping = {};
+
+ /**
+ * mail_msg_struct: structure to orrganize chatter messages
+ */
+ //var mail_msg_struct = {}; // TODO: USE IT OR NOT :)
+
+ /* generic chatter events binding */
+ bind_events: function(widget) {
+ // event: click on an internal link to a document: model, login
+ widget.$element.delegate('a.oe_mail_internal_link', 'click', function (event) {
+ event.preventDefault();
+ // lazy implementation: fetch data and try to redirect
+ if (! event.srcElement.dataset.resModel) return false;
+ else var res_model = event.srcElement.dataset.resModel;
+ var res_login = event.srcElement.dataset.resLogin;
+ if (! res_login) return false;
+ var ds = new session.web.DataSet(widget, res_model);
+ var defer = ds.call('search', [[['login', '=', res_login]]]).pipe(function (records) {
+ if (records[0]) {
+ widget.do_action({ type: 'ir.actions.act_window', res_model: res_model, res_id: parseInt(records[0]), views: [[false, 'form']]});
}
- // add record
- cs['tree_struct'][record.id] = {'level': parent_level+1, 'direct_childs': [], 'all_childs': [], 'for_thread_msgs': [], 'msg_nbr': -1, 'ancestors': []};
- cs['msgs'][record.id] = record;
- modif = true;
- }
+ else return false;
+ });
});
- }
- return cs;
- }
+ },
+
+ /** get an image in /web/binary/image?... */
+ get_image: function(session_prefix, session_id, model, field, id) {
+ return session_prefix + '/web/binary/image?session_id=' + session_id + '&model=' + model + '&field=' + field + '&id=' + (id || '');
+ },
+
+ /** checks if tue current user is the message author */
+ is_author: function (widget, message_user_id) {
+ return (widget.session && widget.session.uid != 0 && widget.session.uid == message_user_id);
+ },
+
+ /**
+ * Add records to comments_structure array
+ * @param {Array} records records from mail.message sorted by date desc
+ * @returns {Object} cs comments_structure: dict
+ * cs.model_to_root_ids = {model: [root_ids], }
+ * cs.new_root_ids = [new_root_ids]
+ * cs.root_ids = [root_ids]
+ * cs.msgs = {record.id: record,}
+ * cs.tree_struct = {record.id: {
+ * 'level': record_level in hierarchy, 0 is root,
+ * 'msg_nbr': number of childs,
+ * 'direct_childs': [msg_ids],
+ * 'all_childs': [msg_ids],
+ * 'for_thread_msgs': [records],
+ * 'ancestors': [msg_ids], } }
+ */
+ records_struct_add_records: function(cs, records, parent_id) {
+ var cur_iter = 0; var max_iter = 10; var modif = true;
+ while ( modif && (cur_iter++) < max_iter) {
+ modif = false;
+ _(records).each(function (record) {
+ // root and not yet recorded
+ if ( (record.parent_id == false || record.parent_id[0] == parent_id) && ! cs['msgs'][record.id]) {
+ // add to model -> root_list ids
+ if (! cs['model_to_root_ids'][record.model]) cs['model_to_root_ids'][record.model] = [record.id];
+ else cs['model_to_root_ids'][record.model].push(record.id);
+ // add root data
+ cs['new_root_ids'].push(record.id);
+ // add record
+ cs['tree_struct'][record.id] = {'level': 0, 'direct_childs': [], 'all_childs': [], 'for_thread_msgs': [record], 'msg_nbr': -1, 'ancestors': []};
+ cs['msgs'][record.id] = record;
+ modif = true;
+ }
+ // not yet recorded, but parent is recorded
+ else if (! cs['msgs'][record.id] && cs['msgs'][record.parent_id[0]]) {
+ var parent_level = cs['tree_struct'][record.parent_id[0]]['level'];
+ // update parent structure
+ cs['tree_struct'][record.parent_id[0]]['direct_childs'].push(record.id);
+ cs['tree_struct'][record.parent_id[0]]['for_thread_msgs'].push(record);
+ // update ancestors structure
+ for (ancestor_id in cs['tree_struct'][record.parent_id[0]]['ancestors']) {
+ cs['tree_struct'][ancestor_id]['all_childs'].push(record.id);
+ }
+ // add record
+ cs['tree_struct'][record.id] = {'level': parent_level+1, 'direct_childs': [], 'all_childs': [], 'for_thread_msgs': [], 'msg_nbr': -1, 'ancestors': []};
+ cs['msgs'][record.id] = record;
+ modif = true;
+ }
+ });
+ }
+ return cs;
+ },
+
+ /* copy cs.new_root_ids into cs.root_ids */
+ records_struct_update_after_display: function(cs) {
+ // update TODO
+ cs['root_ids'] = _.union(cs['root_ids'], cs['new_root_ids']);
+ cs['new_root_ids'] = [];
+ return cs;
+ },
+
+ /**
+ * CONTENT MANIPULATION
+ *
+ * Regular expressions
+ * - (^|\s)@((\w|@|\.)*): @login@log.log, supports inner '@' for
+ * logins that are emails
+ * 1. '(void)'
+ * 2. login@log.log
+ * - (^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]: [ir.attachment,3|My Label],
+ * for internal links to model ir.attachment, id=3, and with
+ * optional label 'My Label'. Note that having a '|Label' is not
+ * mandatory, because the regex should still be correct.
+ * 1. '(void)'
+ * 2. 'ir'
+ * 3. 'attachment'
+ * 4. '3'
+ * 5. 'My Label'
+ */
+
+ /** Removes html tags, except b, em, br, ul, li */
+ do_text_remove_html_tags: function (string) {
+ var html = $('').text(string.replace(/\s+/g, ' ')).html().replace(new RegExp('<(/)?(b|em|br|br /|ul|li|div)\\s*>', 'gi'), '<$1$2>');
+ return html;
+ },
+
+ /** Replaces line breaks by html line breaks (br) */
+ do_text_nl2br: function (str, is_xhtml) {
+ var break_tag = (is_xhtml || typeof is_xhtml === 'undefined') ? ' ' : ' ';
+ return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ break_tag +'$2');
+ },
+
+ /* Add a prefix before each new line of the original string */
+ do_text_quote: function (str, prefix) {
+ return str.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ break_tag +'$2' + prefix || '> ');
+ },
+
+ /**
+ * Replaces some expressions
+ * - @login - shorcut to link to a res.user, given its login
+ * - [ir.attachment,3|My Label] - shortcut to an internal
+ * document
+ * - :name - shortcut to an image
+ */
+ do_replace_expressions: function (string) {
+ var self = this;
+ var icon_list = ['al', 'pinky']
+ /* shortcut to user: @login */
+ var regex_login = new RegExp(/(^|\s)@((\w|@|\.)*)/g);
+ var regex_res = regex_login.exec(string);
+ while (regex_res != null) {
+ var login = regex_res[2];
+ string = string.replace(regex_res[0], regex_res[1] + '@' + login + '');
+ regex_res = regex_login.exec(string);
+ }
+ /* shortcut for internal document */
+ var regex_login = new RegExp(/(^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]/g);
+ var regex_res = regex_login.exec(string);
+ while (regex_res != null) {
+ var res_model = regex_res[2] + '.' + regex_res[3];
+ var res_id = regex_res[4];
+ if (! regex_res[5]) {
+ var label = res_model + ':' + res_id }
+ else {
+ var label = regex_res[5];
+ }
+ string = string.replace(regex_res[0], regex_res[1] + '');
+ regex_res = regex_login.exec(string);
+ }
+ return string;
+ },
+
+ /**
+ * Checks a string to find an expression that will be replaced
+ * by an internal link and requiring a name_get to replace
+ * the expression.
+ * :param mapping: structure to keep a trace of internal links mapping
+ * mapping['model'] = {
+ * name_get': [[id,label], [id,label], ...]
+ * 'to_fetch_ids': [id, id, ...]
+ * }
+ * CURRENTLY NOT IMPLEMENTED */
+ do_check_for_name_get_mapping: function(string, mapping) {
+ /* shortcut to user: @login */
+ //var regex_login = new RegExp(/(^|\s)@((\w|@|\.)*)/g);
+ //var regex_res = regex_login.exec(string);
+ //while (regex_res != null) {
+ //var login = regex_res[2];
+ //if (! ('res.users' in this.map_hash)) { this.map_hash['res.users']['name'] = []; }
+ //this.map_hash['res.users']['login'].push(login);
+ //regex_res = regex_login.exec(string);
+ //}
+ /* document link with name_get: [res.model,name] */
+ /* internal link with id: [res.model,id], or [res.model,id|display_name] */
+ //var regex_intlink = new RegExp(/(^|\s)#(\w*[a-zA-Z_]+\w*)\.(\w+[a-zA-Z_]+\w*),(\w+)/g);
+ //regex_res = regex_intlink.exec(string);
+ //while (regex_res != null) {
+ //var res_model = regex_res[2] + '.' + regex_res[3];
+ //var res_name = regex_res[4];
+ //if (! (res_model in this.map_hash)) { this.map_hash[res_model]['name'] = []; }
+ //this.map_hash[res_model]['name'].push(res_name);
+ //regex_res = regex_intlink.exec(string);
+ //}
+ },
+
+ /**
+ * Updates the mapping; check for to_fetch_ids for each recorded
+ * model, and perform a name_get to update the mapping.
+ * CURRENTLY NOT IMPLEMENTED */
+ do_update_name_get_mapping: function(mapping) {
+ },
+ };
+
+
+ /**
+ * ------------------------------------------------------------
+ * ComposeMessage widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of a form to compose a new message.
+ * This form is an OpenERP form_view, build on a mail.compose.message
+ * wizard.
+ */
+
+ mail.ComposeMessage = session.web.Widget.extend({
+ template: 'mail.compose_message',
+
+ /**
+ * @param {Object} parent parent
+ * @param {Object} [params]
+ * @param {String} [params.res_model] res_model of document [REQUIRED]
+ * @param {Number} [params.res_id] res_id of record [REQUIRED]
+ * @param {Number} [params.email_mode] true/false, tells whether
+ * we are in email sending mode
+ * @param {Number} [params.formatting] true/false, tells whether
+ * we are in advance formatting mode
+ * @param {String} [params.model] mail.compose.message.mode (see
+ * composition wizard)
+ * @param {Number} [params.msg_id] id of a message in case we are in
+ * reply mode
+ */
+ init: function(parent, params) {
+ var self = this;
+ this._super(parent);
+ // options
+ this.params = params || {};
+ this.params.context = params.context || {};
+ this.params.email_mode = params.email_mode || false;
+ this.params.formatting = params.formatting || false;
+ this.params.mode = params.mode || 'comment';
+ this.params.form_xml_id = params.form_xml_id || 'email_compose_message_wizard_form_chatter';
+ this.params.form_view_id = false;
+ if (this.params.mode == 'reply') {
+ this.params.active_id = this.params.msg_id;
+ } else {
+ this.params.active_id = this.params.res_id;
+ }
+ this.email_mode = false;
+ this.formatting = false;
+ },
+
+ /**
+ * Reinitialize the widget field values to the default values. The
+ * purpose is to avoid to destroy and re-build a form view. Default
+ * values are therefore given as for an onchange. */
+ reinit: function() {
+ var self = this;
+ if (! this.form_view) return;
+ var call_defer = this.ds_compose.call('default_get', [['subject', 'body_text', 'body_html', 'dest_partner_ids'], this.ds_compose.get_context()]).then(
+ function (result) {
+ self.form_view.on_processed_onchange({'value': result}, []);
+ });
+ return call_defer;
+ },
+
+ /**
+ * Override-hack of do_action: clean the form */
+ do_action: function(action, on_close) {
+ // this.init_comments();
+ return this._super(action, on_close);
+ },
+
+ /**
+ * Widget start function
+ * - builds and initializes the form view */
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ // customize display: add avatar, clean previous content
+ var user_avatar = mail.ChatterUtils.get_image(this.session.prefix,
+ this.session.session_id, 'res.users', 'avatar', this.session.uid);
+ this.$element.find('img.oe_mail_icon').attr('src', user_avatar);
+ this.$element.find('div.oe_mail_msg_content').empty();
+ // create a context for the default_get of the compose form
+ var widget_context = {
+ 'active_model': this.params.res_model,
+ 'active_id': this.params.active_id,
+ 'mail.compose.message.mode': this.params.mode,
+ };
+ var context = _.extend({}, this.params.context, widget_context);
+ this.ds_compose = new session.web.DataSetSearch(this, 'mail.compose.message', context);
+ // find the id of the view to display in the chatter form
+ var data_ds = new session.web.DataSetSearch(this, 'ir.model.data');
+ var deferred_form_id =data_ds.call('get_object_reference', ['mail', this.params.form_xml_id]).then( function (result) {
+ if (result) {
+ self.params.form_view_id = result[1];
+ }
+ }).pipe(this.proxy('create_form_view'));
+ return deferred_form_id;
+ },
+
+ /**
+ * Create a FormView, then append it to the to widget DOM. */
+ create_form_view: function () {
+ var self = this;
+ // destroy previous form_view if any
+ if (this.form_view) { this.form_view.destroy(); }
+ // create the FormView
+ this.form_view = new session.web.FormView(this, this.ds_compose, this.params.form_view_id, {
+ action_buttons: false,
+ pager: false,
+ initial_mode: 'edit',
+ });
+ // add the form, bind events, activate the form
+ var msg_node = this.$element.find('div.oe_mail_msg_content');
+ return $.when(this.form_view.appendTo(msg_node)).pipe(function() {
+ self.bind_events();
+ self.form_view.do_show();
+ if (self.params.email_mode) { self.toggle_email_mode(); }
+ if (self.params.formatting) { self.toggle_formatting_mode(); }
+ });
+ },
+
+ destroy: function() {
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * Bind events in the widget. Each event is slightly described
+ * in the function. */
+ bind_events: function() {
+ var self = this;
+ this.$element.find('button.oe_form_button').click(function (event) {
+ event.preventDefault();
+ });
+ // event: click on 'Send an Email' link that toggles the form for
+ // sending an email (partner_ids)
+ this.$element.find('a.oe_mail_compose_message_email').click(function (event) {
+ event.preventDefault();
+ self.toggle_email_mode();
+ });
+ // event: click on 'Formatting' icon-link that toggles the advanced
+ // formatting options for writing a message (subject, body_html)
+ this.$element.find('a.oe_mail_compose_message_formatting').click(function (event) {
+ event.preventDefault();
+ self.toggle_formatting_mode();
+ });
+ // event: click on 'Attachment' icon-link that opens the dialog to
+ // add an attachment.
+ this.$element.find('a.oe_mail_compose_message_attachment').click(function (event) {
+ event.preventDefault();
+ // not yet implemented
+ self.set_body_value('attachment', 'attachment');
+ });
+ // event: click on 'Checklist' icon-link that toggles the options
+ // for adding checklist.
+ this.$element.find('a.oe_mail_compose_message_checklist').click(function (event) {
+ event.preventDefault();
+ // not yet implemented
+ self.set_body_value('checklist', 'checklist');
+ });
+ },
+
+ /**
+ * Toggle the formatting mode. */
+ toggle_formatting_mode: function() {
+ var self = this;
+ this.formatting = ! this.formatting;
+ // calls onchange
+ var call_defer = this.ds_compose.call('onchange_formatting', [[], this.formatting, this.params.res_model, this.params.res_id]).then(
+ function (result) {
+ self.form_view.on_processed_onchange(result, []);
+ });
+ // update context of datasetsearch
+ this.ds_compose.context.formatting = this.formatting;
+ // toggle display
+ this.$element.find('span.oe_mail_compose_message_subject').toggleClass('oe_mail_compose_message_invisible');
+ this.$element.find('div.oe_mail_compose_message_body_text').toggleClass('oe_mail_compose_message_invisible');
+ this.$element.find('div.oe_mail_compose_message_body_html').toggleClass('oe_mail_compose_message_invisible');
+ },
+
+ /**
+ * Toggle the email mode. */
+ toggle_email_mode: function() {
+ var self = this;
+ this.email_mode = ! this.email_mode;
+ // calls onchange
+ var call_defer = this.ds_compose.call('onchange_email_mode', [[], this.email_mode, this.params.res_model, this.params.res_id]).then(
+ function (result) {
+ self.form_view.on_processed_onchange(result, []);
+ });
+ // update context of datasetsearch
+ this.ds_compose.context.email_mode = this.email_mode;
+ // update 'Post' button -> 'Send'
+ // update 'Send an Email' link -> 'Post a comment'
+ if (this.email_mode) {
+ this.$element.find('button.oe_mail_compose_message_button_send').html('Send');
+ this.$element.find('a.oe_mail_compose_message_email').html('Comment');
+ } else {
+ this.$element.find('button.oe_mail_compose_message_button_send').html('Post');
+ this.$element.find('a.oe_mail_compose_message_email').html('Send an Email');
+ }
+ // toggle display
+ this.$element.find('div.oe_mail_compose_message_partner_ids').toggleClass('oe_mail_compose_message_invisible');
+ },
+
+ /**
+ * Update the values of the composition form; with possible different
+ * values for body_text and body_html. */
+ set_body_value: function(body_text, body_html) {
+ this.form_view.fields.body_text.set_value(body_text);
+ this.form_view.fields.body_html.set_value(body_html);
+ },
+ }),
/**
- * ThreadDisplay widget: this widget handles the display of a thread of
- * messages. The [thread_level] parameter sets the thread level number:
+ * ------------------------------------------------------------
+ * Thread Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of a thread of messages. The
+ * [thread_level] parameter sets the thread level number:
* - root message
* - - sub message (parent_id = root message)
* - - - sub sub message (parent id = sub message)
* - - sub message (parent_id = root message)
- * This widget has 2 ways of initialization, either you give records to be rendered,
- * either it will fetch [limit] messages related to [res_model]:[res_id].
+ * This widget has 2 ways of initialization, either you give records
+ * to be rendered, either it will fetch [limit] messages related to
+ * [res_model]:[res_id].
*/
+
mail.Thread = session.web.Widget.extend({
- template: 'mail.Thread',
+ template: 'mail.thread',
/**
* @param {Object} parent parent
* @param {Object} [params]
- * @param {String} [params.res_model] res_model of mail.thread object
- * @param {Number} [params.res_id] res_id of record
- * @param {Number} [params.parent_id=false] parent_id of message
- * @param {Number} [params.uid] user id
- * @param {Number} [params.thread_level=0] number of levels in the thread (only 0 or 1 currently)
- * @param {Number} [params.msg_more_limit=100] number of character to display before having a "show more" link;
- * note that the text will not be truncated if it does not have 110% of
- * the parameter (ex: 110 characters needed to be truncated and be displayed
- * as a 100-characters message)
- * @param {Number} [params.limit=10] maximum number of messages to fetch
+ * @param {String} [params.res_model] res_model of document [REQUIRED]
+ * @param {Number} [params.res_id] res_id of record [REQUIRED]
+ * @param {Number} [params.uid] user id [REQUIRED]
+ * @param {Bool} [params.parent_id=false] parent_id of message
+ * @param {Number} [params.thread_level=0] number of levels in the thread
+ * (only 0 or 1 currently)
+ * @param {Bool} [params.is_wall=false] thread is displayed in the wall
+ * @param {Number} [params.msg_more_limit=150] number of character to
+ * display before having a "show more" link; note that the text
+ * will not be truncated if it does not have 110% of the parameter
+ * (ex: 110 characters needed to be truncated and be displayed as
+ * a 100-characters message)
+ * @param {Number} [params.limit=100] maximum number of messages to fetch
* @param {Number} [params.offset=0] offset for fetching messages
* @param {Number} [params.records=null] records to show instead of fetching messages
*/
init: function(parent, params) {
this._super(parent);
+ // options
this.params = params;
this.params.parent_id = this.params.parent_id || false;
this.params.thread_level = this.params.thread_level || 0;
- this.params.msg_more_limit = this.params.msg_more_limit || 290;
+ this.params.is_wall = this.params.is_wall || (this.params.records != undefined) || false;
+ this.params.msg_more_limit = this.params.msg_more_limit || 150;
this.params.limit = this.params.limit || 100;
+ // this.params.limit = 3; // tmp for testing
this.params.offset = this.params.offset || 0;
this.params.records = this.params.records || null;
// datasets and internal vars
- this.ds = new session.web.DataSet(this, this.params.res_model);
- this.ds_users = new session.web.DataSet(this, 'res.users');
- this.ds_msg = new session.web.DataSet(this, 'mail.message');
- this.sorted_comments = {'root_ids': [], 'root_id_msg_list': {}};
+ this.ds = new session.web.DataSetSearch(this, this.params.res_model);
+ this.ds_users = new session.web.DataSetSearch(this, 'res.users');
+ this.ds_msg = new session.web.DataSetSearch(this, 'mail.message');
this.comments_structure = {'root_ids': [], 'new_root_ids': [], 'msgs': {}, 'tree_struct': {}, 'model_to_root_ids': {}};
// display customization vars
this.display = {};
this.display.show_post_comment = this.params.show_post_comment || false;
- this.display.show_reply = (this.params.thread_level > 0);
- this.display.show_delete = true;
- this.display.show_hide = this.params.show_hide || false;
+ this.display.show_reply = (this.params.thread_level > 0 && this.params.is_wall);
+ this.display.show_delete = ! this.params.is_wall;
+ this.display.show_hide = this.params.is_wall;
+ this.display.show_reply_by_email = ! this.params.is_wall;
this.display.show_more = (this.params.thread_level == 0);
- // not used currently
- this.intlinks_mapping = {};
},
start: function() {
- var self = this;
-
this._super.apply(this, arguments);
- // customize display
- if (! this.display.show_post_comment) {
- this.$element.find('div.oe_mail_thread_action').hide();
- }
// add events
- this.add_events();
-
+ this.bind_events();
// display user, fetch comments
this.display_current_user();
-
- if (this.params.records) {
- var display_done = this.display_comments_from_parameters(this.params.records);
- } else {
- var display_done = this.init_comments();
+ if (this.params.records) var display_done = this.display_comments_from_parameters(this.params.records);
+ else var display_done = this.init_comments();
+ // customize display
+ $.when(display_done).then(this.proxy('do_customize_display'));
+ // add message composition form view
+ if (this.display.show_post_comment) {
+ var compose_done = this.instantiate_composition_form();
}
- return display_done
+ return display_done && compose_done;
},
-
- add_events: function() {
+
+ /**
+ * Override-hack of do_action: automatically reload the chatter.
+ * Normally it should be called only when clicking on 'Post/Send'
+ * in the composition form. */
+ do_action: function(action, on_close) {
+ this.init_comments();
+ if (this.compose_message_widget) {
+ this.compose_message_widget.reinit(); }
+ return this._super(action, on_close);
+ },
+
+ instantiate_composition_form: function(mode, email_mode, formatting, msg_id, context) {
+ if (this.compose_message_widget) {
+ this.compose_message_widget.destroy();
+ }
+ this.compose_message_widget = new mail.ComposeMessage(this, {
+ 'extended_mode': false, 'uid': this.params.uid, 'res_model': this.params.res_model,
+ 'res_id': this.params.res_id, 'mode': mode || 'comment', 'msg_id': msg_id,
+ 'email_mode': email_mode || false, 'formatting': formatting || false,
+ 'context': context || false } );
+ var composition_node = this.$element.find('div.oe_mail_thread_action');
+ composition_node.empty();
+ var compose_done = this.compose_message_widget.appendTo(composition_node);
+ return compose_done;
+ },
+
+ do_customize_display: function() {
+ if (this.display.show_post_comment) { this.$element.find('div.oe_mail_thread_action').eq(0).show(); }
+ },
+
+ /**
+ * Bind events in the widget. Each event is slightly described
+ * in the function. */
+ bind_events: function() {
var self = this;
+ // generic events from Chatter Mixin
+ mail.ChatterUtils.bind_events(this);
// event: click on 'more' at bottom of thread
this.$element.find('button.oe_mail_button_more').click(function () {
self.do_more();
});
- // event: writing in textarea
- this.$element.find('textarea.oe_mail_action_textarea').keyup(function (event) {
+ // event: writing in basic textarea of composition form (quick reply)
+ this.$element.find('textarea.oe_mail_compose_textarea').keyup(function (event) {
var charCode = (event.which) ? event.which : window.event.keyCode;
if (event.shiftKey && charCode == 13) { this.value = this.value+"\n"; }
else if (charCode == 13) { return self.do_comment(); }
});
- // event: click on 'reply' in msg
+ // event: click on 'Reply' in msg
this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_reply', 'click', function (event) {
var act_dom = $(this).parents('div.oe_mail_thread_display').find('div.oe_mail_thread_action:first');
act_dom.toggle();
event.preventDefault();
});
- // event: click on 'delete' in msg
- this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_delete', 'click', function (event) {
- //console.log('deleting');
- if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
- var msg_id = event.srcElement.dataset.id;
- if (! msg_id) return false;
- var call_defer = self.ds_msg.unlink([parseInt(msg_id)]);
- $(event.srcElement).parents('.oe_mail_thread_msg').eq(0).hide();
- if (self.params.thread_level > 0) {
- $(event.srcElement).parents('.oe_mail_thread').eq(0).hide();
- }
- return false;
- });
- // event: click on 'hide' in msg
- this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_hide', 'click', function (event) {
- //console.log('hiding');
- if (! confirm(_t("Do you really want to hide this thread ?"))) { return false; }
- var msg_id = event.srcElement.dataset.id;
- if (! msg_id) return false;
- //console.log(msg_id);
- var call_defer = self.ds.call('message_remove_pushed_notifications', [[self.params.res_id], [parseInt(msg_id)], true]);
- $(event.srcElement).parents('.oe_mail_thread_msg').eq(0).hide();
- if (self.params.thread_level > 0) {
- $(event.srcElement).parents('.oe_mail_thread').eq(0).hide();
- }
- return false;
- });
- // event: click on an internal link
- this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_internal_link', 'click', function (event) {
- // lazy implementation: fetch data and try to redirect
- if (! event.srcElement.dataset.resModel) return false;
- else var res_model = event.srcElement.dataset.resModel;
- var res_login = event.srcElement.dataset.resLogin;
- var res_id = event.srcElement.dataset.resId;
- if ((! res_login) && (! res_id)) return false;
- if (! res_id) {
- var ds = new session.web.DataSet(self, res_model);
- var defer = ds.call('search', [[['login', '=', res_login]]]).then(function (records) {
- if (records[0]) {
- self.do_action({ type: 'ir.actions.act_window', res_model: res_model, res_id: parseInt(records[0]), views: [[false, 'form']]});
- }
- else return false;
- });
- }
- else self.do_action({ type: 'ir.actions.act_window', res_model: res_model, res_id: parseInt(res_id), views: [[false, 'form']]});
- });
// event: click on 'attachment(s)' in msg
this.$element.delegate('a.oe_mail_msg_view_attachments', 'click', function (event) {
var act_dom = $(this).parent().parent().parent().find('.oe_mail_msg_attachments');
act_dom.toggle();
- return false;
+ event.preventDefault();
});
- // see more
- this.$element.on('click','a.oe_mail_msg_more', function (event) {
- $(this).siblings('.oe_mail_msg_tail').show();
- $(this).hide();
- return false;
+ // event: click on 'Delete' in msg side menu
+ this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_delete', 'click', function (event) {
+ if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
+ var msg_id = event.srcElement.dataset.id;
+ if (! msg_id) return false;
+ var call_defer = self.ds_msg.unlink([parseInt(msg_id)]);
+ $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).hide();
+ if (self.params.thread_level > 0) {
+ $(event.srcElement).parents('.oe_mail_thread').eq(0).hide();
+ }
+ event.preventDefault();
+ return call_defer;
+ });
+ // event: click on 'Hide' in msg side menu
+ this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_hide', 'click', function (event) {
+ if (! confirm(_t("Do you really want to hide this thread ?"))) { return false; }
+ var msg_id = event.srcElement.dataset.id;
+ if (! msg_id) return false;
+ var call_defer = self.ds.call('message_remove_pushed_notifications', [[self.params.res_id], [parseInt(msg_id)], true]);
+ $(event.srcElement).parents('li.oe_mail_thread_msg').eq(0).hide();
+ if (self.params.thread_level > 0) {
+ $(event.srcElement).parents('.oe_mail_thread').eq(0).hide();
+ }
+ event.preventDefault();
+ return call_defer;
+ });
+ // event: click on "Reply" in msg side menu (email style)
+ this.$element.find('div.oe_mail_thread_display').delegate('a.oe_mail_msg_reply_by_email', 'click', function (event) {
+ var msg_id = event.srcElement.dataset.msg_id;
+ var email_mode = (event.srcElement.dataset.type == 'email');
+ var formatting = (event.srcElement.dataset.formatting == 'html');
+ if (! msg_id) return false;
+ self.instantiate_composition_form('reply', email_mode, formatting, msg_id);
+ event.preventDefault();
});
},
@@ -219,21 +661,22 @@ openerp.mail = function(session) {
init_comments: function() {
var self = this;
this.params.offset = 0;
- this.sorted_comments = {'root_ids': [], 'root_id_msg_list': {}};
this.comments_structure = {'root_ids': [], 'new_root_ids': [], 'msgs': {}, 'tree_struct': {}, 'model_to_root_ids': {}};
this.$element.find('div.oe_mail_thread_display').empty();
- domain = this.get_fetch_domain(this.sorted_comments);
+ var domain = this.get_fetch_domain(this.comments_structure);
return this.fetch_comments(this.params.limit, this.params.offset, domain).then();
},
fetch_comments: function (limit, offset, domain) {
var self = this;
- var defer = this.ds.call('message_load', [[this.params.res_id], ( (limit+1)||(this.params.limit+1) ), (offset||this.params.offset), (domain||[]), (this.params.thread_level > 0), (this.sorted_comments['root_ids'])]);
- $.when(defer).then(function (records) {
+ var defer = this.ds.call('message_read', [[this.params.res_id], (this.params.thread_level > 0), (this.comments_structure['root_ids']),
+ (limit+1) || (this.params.limit+1), offset||this.params.offset, domain||undefined ]).then(function (records) {
if (records.length <= self.params.limit) self.display.show_more = false;
- else { self.display.show_more = true; records.pop(); }
-
+ // else { self.display.show_more = true; records.pop(); }
+ // else { self.display.show_more = true; records.splice(0, 1); }
+ else { self.display.show_more = true; }
self.display_comments(records);
+ // TODO: move to customize display
if (self.display.show_more == true) self.$element.find('div.oe_mail_thread_more:last').show();
else self.$element.find('div.oe_mail_thread_more:last').hide();
});
@@ -245,6 +688,7 @@ openerp.mail = function(session) {
if (records.length > 0 && records.length < (records[0].child_ids.length+1) ) this.display.show_more = true;
else this.display.show_more = false;
var defer = this.display_comments(records);
+ // TODO: move to customize display
if (this.display.show_more == true) $('div.oe_mail_thread_more').eq(-2).show();
else $('div.oe_mail_thread_more').eq(-2).hide();
return defer;
@@ -252,7 +696,8 @@ openerp.mail = function(session) {
display_comments: function (records) {
var self = this;
-
+ // sort the records
+ mail.ChatterUtils.records_struct_add_records(this.comments_structure, records, this.params.parent_id);
//build attachments download urls and compute time-relative from dates
for (var k in records) {
records[k].timerelative = $.timeago(records[k].date);
@@ -263,11 +708,10 @@ openerp.mail = function(session) {
}
}
}
- this.cs = this.sort_comments_tmp(records);
_(records).each(function (record) {
var sub_msgs = [];
if ((record.parent_id == false || record.parent_id[0] == self.params.parent_id) && self.params.thread_level > 0 ) {
- var sub_list = self.cs['tree_struct'][record.id]['direct_childs'];
+ var sub_list = self.comments_structure['tree_struct'][record.id]['direct_childs'];
_(records).each(function (record) {
//if (record.parent_id == false || record.parent_id[0] == self.params.parent_id) return;
if (_.indexOf(sub_list, record.id) != -1) {
@@ -276,35 +720,40 @@ openerp.mail = function(session) {
});
self.display_comment(record);
self.thread = new mail.Thread(self, {'res_model': self.params.res_model, 'res_id': self.params.res_id, 'uid': self.params.uid,
- 'records': sub_msgs, 'thread_level': (self.params.thread_level-1), 'parent_id': record.id});
- self.$element.find('.oe_mail_thread_msg:last').append('');
+ 'records': sub_msgs, 'thread_level': (self.params.thread_level-1), 'parent_id': record.id,
+ 'is_wall': self.params.is_wall});
+ self.$element.find('li.oe_mail_thread_msg:last').append('');
self.thread.appendTo(self.$element.find('div.oe_mail_thread_subthread:last'));
}
else if (self.params.thread_level == 0) {
self.display_comment(record);
}
});
+ mail.ChatterUtils.records_struct_update_after_display(this.comments_structure);
// update offset for "More" buttons
if (this.params.thread_level == 0) this.params.offset += records.length;
},
- /**
- * Display a record
- */
+ /** Displays a record, performs text/link formatting */
display_comment: function (record) {
- record.body = this.do_text_nl2br(record.body, true);
+ record.body = mail.ChatterUtils.do_text_nl2br(record.body, true);
+ // if (record.type == 'email' && record.state == 'received') {
if (record.type == 'email') {
record.mini_url = ('/mail/static/src/img/email_icon.png');
- } else {
- record.mini_url = this.thread_get_avatar('res.users', 'avatar', record.user_id[0]);
+ } else {
+ record.mini_url = mail.ChatterUtils.get_image(this.session.prefix, this.session.session_id, 'res.users', 'avatar', record.user_id[0]);
}
// body text manipulation
- record.body = this.do_clean_text(record.body);
- record.body = this.do_replace_internal_links(record.body);
-
+ if (record.subtype == 'plain') {
+ record.body = mail.ChatterUtils.do_text_remove_html_tags(record.body);
+ }
+ record.body = mail.ChatterUtils.do_replace_expressions(record.body);
// format date according to the user timezone
record.date = session.web.format_value(record.date, {type:"datetime"});
- var rendered = session.web.qweb.render('mail.Thread.message', {'record': record, 'thread': this, 'params': this.params, 'display': this.display});
+ // is the user the author ?
+ record.is_author = mail.ChatterUtils.is_author(this, record.user_id[0]);
+ // render
+ var rendered = session.web.qweb.render('mail.thread.message', {'record': record, 'thread': this, 'params': this.params, 'display': this.display});
$(rendered).appendTo(this.$element.children('div.oe_mail_thread_display:first'));
// expand feature
this.$element.find('div.oe_mail_msg_body:last').expander({
@@ -316,84 +765,33 @@ openerp.mail = function(session) {
lesClass: 'oe_mail_reduce',
});
},
-
- /**
- * Add records to sorted_comments array
- * @param {Array} records records from mail.message sorted by date desc
- * @returns {Object} sc sorted_comments: dict {
- * 'root_id_list': list or root_ids
- * 'root_id_msg_list': {'record_id': [ancestor_ids]}, still sorted by date desc
- * 'id_to_root': {'root_id': [records]}, still sorted by date desc
- * }
- */
- sort_comments: function (records) {
- var self = this;
- sc = {'root_id_list': [], 'root_id_msg_list': {}, 'id_to_root': {}}
- var cur_iter = 0; var max_iter = 10; var modif = true;
- /* step1: get roots */
- while ( modif && (cur_iter++) < max_iter) {
- modif = false;
- _(records).each(function (record) {
- if ( (record.parent_id == false || record.parent_id[0] == self.params.parent_id) && (_.indexOf(sc['root_id_list'], record.id) == -1)) {
- sc['root_id_list'].push(record.id);
- sc['root_id_msg_list'][record.id] = [];
- self.sorted_comments['root_ids'].push(record.id);
- modif = true;
- }
- else {
- if (_.indexOf(sc['root_id_list'], record.parent_id[0]) != -1) {
- sc['id_to_root'][record.id] = record.parent_id[0];
- modif = true;
- }
- else if ( sc['id_to_root'][record.parent_id[0]] ) {
- sc['id_to_root'][record.id] = sc['id_to_root'][record.parent_id[0]];
- modif = true;
- }
- }
- });
- }
- /* step2: add records */
- _(records).each(function (record) {
- var root_id = sc['id_to_root'][record.id];
- if (! root_id) return;
- sc['root_id_msg_list'][root_id].push(record);
- //self.sorted_comments['root_id_msg_list'][root_id].push(record.id);
- });
- return sc;
- },
-
- /**
- * Add records to comments_structure object: see function for details
- */
- sort_comments_tmp: function(records) {
- return tools_sort_comments(this.comments_structure, records, this.params.parent_id);
- },
-
+
display_current_user: function () {
- return this.$element.find('img.oe_mail_msg_image').attr('src', this.thread_get_avatar('res.users', 'avatar', this.params.uid));
+ var avatar = mail.ChatterUtils.get_image(this.session.prefix, this.session.session_id, 'res.users', 'avatar', this.params.uid);
+ return this.$element.find('img.oe_mail_icon').attr('src', avatar);
},
do_comment: function () {
var comment_node = this.$element.find('textarea');
var body_text = comment_node.val();
comment_node.val('');
- return this.ds.call('message_append_note', [[this.params.res_id], 'Reply', body_text, this.params.parent_id, 'comment', 'html']).then(
+ return this.ds.call('message_append_note', [[this.params.res_id], '', body_text, this.params.parent_id, 'comment', 'plain']).then(
this.proxy('init_comments'));
},
/**
* Create a domain to fetch new comments according to
- * comment already present in sorted_comments
- * @param {Object} sorted_comments (see sort_comments)
+ * comment already present in comments_structure
+ * @param {Object} comments_structure (see chatter utils)
* @returns {Array} fetch_domain (OpenERP domain style)
*/
- get_fetch_domain: function (sorted_comments) {
+ get_fetch_domain: function (comments_structure) {
var domain = [];
- var ids = sorted_comments.root_ids.slice();
+ var ids = comments_structure.root_ids.slice();
var ids2 = [];
// must be child of current parent
if (this.params.parent_id) { domain.push(['id', 'child_of', this.params.parent_id]); }
- _(sorted_comments.root_ids).each(function (id) { // each record
+ _(comments_structure.root_ids).each(function (id) { // each record
ids.push(id);
ids2.push(id);
});
@@ -413,109 +811,46 @@ openerp.mail = function(session) {
},
do_more: function () {
- domain = this.get_fetch_domain(this.sorted_comments);
+ domain = this.get_fetch_domain(this.comments_structure);
return this.fetch_comments(this.params.limit, this.params.offset, domain);
},
-
- /**
- *
- * var regex_login = new RegExp(/(^|\s)@((\w|@|\.)*)/g);
- * var regex_intlink = new RegExp(/(^|\s)#(\w*[a-zA-Z_]+\w*)\.(\w+[a-zA-Z_]+\w*),(\w+)/g);
- */
- do_replace_internal_links: function (string) {
- var self = this;
- var icon_list = ['al', 'pinky']
- /* shortcut to user: @login */
- var regex_login = new RegExp(/(^|\s)@((\w|@|\.)*)/g);
- var regex_res = regex_login.exec(string);
- while (regex_res != null) {
- var login = regex_res[2];
- string = string.replace(regex_res[0], regex_res[1] + '@' + login + '');
- regex_res = regex_login.exec(string);
- }
- /* special shortcut: :name, try to find an icon if in list */
- var regex_login = new RegExp(/(^|\s):((\w)*)/g);
- var regex_res = regex_login.exec(string);
- while (regex_res != null) {
- var icon_name = regex_res[2];
- if (_.include(icon_list, icon_name))
- string = string.replace(regex_res[0], regex_res[1] + '');
- regex_res = regex_login.exec(string);
- }
- return string;
- },
-
- thread_get_avatar: function(model, field, id) {
- return this.session.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + (id || '');
- },
-
- /** Removes html tags, except b, em, br */
- do_clean_text: function (string) {
- var html = $('').text(string.replace(/\s+/g, ' ')).html().replace(new RegExp('<(/)?(b|em|br|br /)\\s*>', 'gi'), '<$1$2>');
- return html;
- },
-
- /** Replaces line bracks by html line breaks (br) */
- do_text_nl2br: function (str, is_xhtml) {
- var break_tag = (is_xhtml || typeof is_xhtml === 'undefined') ? ' ' : ' ';
- return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ break_tag +'$2');
- },
-
- /**
- *
- * var regex_login = new RegExp(/(^|\s)@((\w|@|\.)*)/g);
- * var regex_intlink = new RegExp(/(^|\s)#(\w*[a-zA-Z_]+\w*)\.(\w+[a-zA-Z_]+\w*),(\w+)/g);
- */
- do_check_internal_links: function(string) {
- /* shortcut to user: @login */
- var regex_login = new RegExp(/(^|\s)@((\w|@|\.)*)/g);
- var regex_res = regex_login.exec(string);
- while (regex_res != null) {
- var login = regex_res[2];
- if (! ('res.users' in this.map_hash)) { this.map_hash['res.users']['name'] = []; }
- this.map_hash['res.users']['login'].push(login);
- regex_res = regex_login.exec(string);
- }
- /* internal links: #res.model,name */
- var regex_intlink = new RegExp(/(^|\s)#(\w*[a-zA-Z_]+\w*)\.(\w+[a-zA-Z_]+\w*),(\w+)/g);
- regex_res = regex_intlink.exec(string);
- while (regex_res != null) {
- var res_model = regex_res[2] + '.' + regex_res[3];
- var res_name = regex_res[4];
- if (! (res_model in this.map_hash)) { this.map_hash[res_model]['name'] = []; }
- this.map_hash[res_model]['name'].push(res_name);
- regex_res = regex_intlink.exec(string);
- }
- },
-
- /** checks if tue current user is the message author */
- _is_author: function (id) {
- return (this.session.uid == id);
- },
-
});
- session.web.form.widgets.add( 'Thread', 'openerp.mail.Thread');
+
+
+ /**
+ * ------------------------------------------------------------
+ * mail_thread Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of the Chatter on documents.
+ */
+
+ /* Add mail_thread widget to registry */
+ session.web.form.widgets.add('mail_thread', 'openerp.mail.RecordThread');
/** mail_thread widget: thread of comments */
mail.RecordThread = session.web.form.AbstractField.extend({
- template: 'mail.RecordThread',
+ // QWeb template to use when rendering the object
+ template: 'mail.record_thread',
init: function() {
this._super.apply(this, arguments);
- this.see_subscribers = true;
- this.thread = null;
this.params = this.get_definition_options();
this.params.thread_level = this.params.thread_level || 0;
- // datasets
+ this.params.see_subscribers = true;
+ this.params.see_subscribers_options = this.params.see_subscribers_options || false;
+ this.thread = null;
this.ds = new session.web.DataSet(this, this.view.model);
this.ds_users = new session.web.DataSet(this, 'res.users');
},
start: function() {
- this._super.apply(this, arguments);
var self = this;
- // bind buttons
- this.$element.find('button.oe_mail_button_followers').click(function () { self.do_toggle_followers(); }).hide();
+ this._super.apply(this, arguments);
+ mail.ChatterUtils.bind_events(this);
+ this.$element.find('button.oe_mail_button_followers').click(function () { self.do_toggle_followers(); });
+ if (! this.params.see_subscribers_options) {
+ this.$element.find('button.oe_mail_button_followers').hide(); }
this.$element.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'); });
@@ -530,7 +865,8 @@ openerp.mail = function(session) {
},
reinit: function() {
- this.see_subscribers = true;
+ this.params.see_subscribers = true;
+ this.params.see_subscribers_options = this.params.see_subscribers_options || false;
this.$element.find('button.oe_mail_button_followers').html('Hide followers')
this.$element.find('button.oe_mail_button_follow').hide();
this.$element.find('button.oe_mail_button_unfollow').hide();
@@ -548,11 +884,11 @@ openerp.mail = function(session) {
// fetch followers
var fetch_sub_done = this.fetch_subscribers();
// create and render Thread widget
- this.$element.find('div.oe_mail_recthread_left').empty();
+ this.$element.find('div.oe_mail_recthread_main').empty();
if (this.thread) this.thread.destroy();
this.thread = new mail.Thread(this, {'res_model': this.view.model, 'res_id': this.view.datarecord.id, 'uid': this.session.uid,
- 'thread_level': this.params.thread_level, 'show_post_comment': true, 'limit': 15});
- var thread_done = this.thread.appendTo(this.$element.find('div.oe_mail_recthread_left'));
+ 'thread_level': this.params.thread_level, 'show_post_comment': true, 'limit': 15});
+ var thread_done = this.thread.appendTo(this.$element.find('div.oe_mail_recthread_main'));
return fetch_sub_done && thread_done;
},
@@ -567,9 +903,8 @@ openerp.mail = function(session) {
this.$element.find('div.oe_mail_recthread_followers h4').html('Followers (' + records.length + ')');
_(records).each(function (record) {
if (record.id == self.session.uid) { self.is_subscriber = true; }
- var mini_url = self.thread_get_avatar('res.users', 'avatar', record.id);
- $('
+
+
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 1b19a383c10..d26511464f8 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -33,7 +33,7 @@ from ..mail_message import to_email
# main mako-like expression pattern
EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
-class mail_compose_message(osv.osv_memory):
+class mail_compose_message(osv.TransientModel):
"""Generic Email composition wizard. This wizard is meant to be inherited
at model and view level to provide specific wizard features.
@@ -41,13 +41,12 @@ class mail_compose_message(osv.osv_memory):
parameters, among which are:
* mail.compose.message.mode: if set to 'reply', the wizard is in
- reply mode and pre-populated with the original quote.
- If set to 'mass_mail', the wizard is in mass mailing
- where the mail details can contain template placeholders
- that will be merged with actual data before being sent
- to each recipient. Recipients will be derived from the
- records determined via ``context['active_model']`` and
- ``context['active_ids']``.
+ reply to a previous message mode and pre-populated with the original
+ quote. If set to 'comment', it means you are writing a new message to
+ be attached to a document. If set to 'mass_mail', the wizard is in
+ mass mailing where the mail details can contain template placeholders
+ that will be merged with actual data before being sent to each
+ recipient.
* active_model: model name of the document to which the mail being
composed is related
* active_id: id of the document to which the mail being composed is
@@ -61,119 +60,179 @@ class mail_compose_message(osv.osv_memory):
_description = 'Email composition wizard'
def default_get(self, cr, uid, fields, context=None):
- """Overridden to provide specific defaults depending on the context
- parameters.
+ """ Overridden to provide specific defaults depending on the context
+ parameters.
+
+ Composition mode
+ - comment: default mode; active_model, active_id = model and ID of a
+ document we are commenting,
+ - reply: active_id = ID of a mail.message to which we are replying.
+ From this message we can find the related model and res_id,
+ - mass_mailing mode: active_model, active_id = model and ID of a
+ document we are commenting,
:param dict context: several context values will modify the behavior
of the wizard, cfr. the class description.
"""
if context is None:
context = {}
+ compose_mode = context.get('mail.compose.message.mode', 'comment')
+ active_model = context.get('active_model')
+ active_id = context.get('active_id')
result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
+
+ # get default values according to the composition mode
vals = {}
- reply_mode = context.get('mail.compose.message.mode') == 'reply'
- if (not reply_mode) and context.get('active_model') and context.get('active_id'):
- # normal mode when sending an email related to any document, as specified by
- # active_model and active_id in context
- vals = self.get_value(cr, uid, context.get('active_model'), context.get('active_id'), context)
- elif reply_mode and context.get('active_id'):
- # reply mode, consider active_id is the ID of a mail.message to which we're
- # replying
- vals = self.get_message_data(cr, uid, int(context['active_id']), context)
- else:
- # default mode
- result['model'] = context.get('active_model', False)
+ if compose_mode in ['reply']:
+ vals = self.get_message_data(cr, uid, int(context['active_id']), context=context)
+ elif compose_mode in ['comment', 'mass_mail'] and active_model and active_id:
+ vals = self.get_value(cr, uid, active_model, active_id, context)
for field in vals:
if field in fields:
- result.update({field : vals[field]})
+ result[field] = vals[field]
# link to model and record if not done yet
- if not result.get('model') or not result.get('res_id'):
- active_model = context.get('active_model')
- res_id = context.get('active_id')
- if active_model and active_model not in (self._name, 'mail.message'):
- result['model'] = active_model
- if res_id:
- result['res_id'] = res_id
+ if not result.get('model') and active_model:
+ result['model'] = active_model
+ if not result.get('res_id') and active_id:
+ result['res_id'] = active_id
# Try to provide default email_from if not specified yet
if not result.get('email_from'):
- current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
result['email_from'] = current_user.user_email or False
return result
_columns = {
+ 'dest_partner_ids': fields.many2many('res.partner',
+ 'email_message_send_partner_rel',
+ 'wizard_id', 'partner_id', 'Destination partners',
+ help="When sending emails through the social network composition wizard"\
+ "you may choose to send a copy of the mail to partners."),
'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
'filter_id': fields.many2one('ir.filters', 'Filters'),
}
def get_value(self, cr, uid, model, res_id, context=None):
- """Returns a defaults-like dict with initial values for the composition
- wizard when sending an email related to the document record identified
- by ``model`` and ``res_id``.
+ """ Returns a defaults-like dict with initial values for the composition
+ wizard when sending an email related to the document record
+ identified by ``model`` and ``res_id``.
- The default implementation returns an empty dictionary, and is meant
- to be overridden by subclasses.
+ The default implementation returns an empty dictionary, and is meant
+ to be overridden by subclasses.
- :param str model: model name of the document record this mail is related to.
- :param int res_id: id of the document record this mail is related to.
- :param dict context: several context values will modify the behavior
- of the wizard, cfr. the class description.
+ :param str model: model name of the document record this mail is
+ related to.
+ :param int res_id: id of the document record this mail is related to.
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
"""
- return {}
+ result = {}
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ result.update({
+ 'model': model,
+ 'res_id': res_id,
+ 'email_from': user.user_email or tools.config.get('email_from', False),
+ 'body_html': False,
+ 'body_text': False,
+ 'subject': False,
+ 'dest_partner_ids': [],
+ })
+ return result
+
+ def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None):
+ """ email_mode (values: True or False). This onchange on the email mode
+ allows to have some specific behavior when going in email mode, or
+ when going out of email mode.
+ Basically, dest_partner_ids is reset when going out of email
+ mode.
+ This method can be overridden for models that want to have their
+ specific behavior.
+ Note that currently, this onchange is used in mail.js and called
+ manually on the form instantiated in the Chatter.
+ """
+ if not value:
+ return {'value': {'dest_partner_ids': []}}
+ return {'value': {}}
+
+ def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None):
+ """ onchange_formatting (values: True or False). This onchange on the
+ formatting allows to have some specific behavior when going in
+ formatting mode, or when going out of formatting.
+ Basically, subject is reset when going out of formatting mode.
+ This method can be overridden for models that want to have their
+ specific behavior.
+ Note that currently, this onchange is used in mail.js and called
+ manually on the form instantiated in the Chatter.
+ """
+ if not value:
+ return {'value': {'subject': False}}
+ return {'value': {}}
def get_message_data(self, cr, uid, message_id, context=None):
- """Returns a defaults-like dict with initial values for the composition
- wizard when replying to the given message (e.g. including the quote
- of the initial message, and the correct recipient).
- Should not be called unless ``context['mail.compose.message.mode'] == 'reply'``.
+ """ Returns a defaults-like dict with initial values for the composition
+ wizard when replying to the given message (e.g. including the quote
+ of the initial message, and the correct recipient). It should not be
+ called unless ``context['mail.compose.message.mode'] == 'reply'``.
- :param int message_id: id of the mail.message to which the user
- is replying.
- :param dict context: several context values will modify the behavior
- of the wizard, cfr. the class description.
- When calling this method, the ``'mail'`` value
- in the context should be ``'reply'``.
+ :param int message_id: id of the mail.message to which the user
+ is replying.
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
"""
if context is None:
context = {}
result = {}
- mail_message = self.pool.get('mail.message')
- if message_id:
- message_data = mail_message.browse(cr, uid, message_id, context)
- subject = tools.ustr(message_data.subject or '')
- # we use the plain text version of the original mail, by default,
- # as it is easier to quote than the HTML version.
- # XXX TODO: make it possible to switch to HTML on the fly
- current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
- body = message_data.body_text or current_user.signature or ''
- if context.get('mail.compose.message.mode') == 'reply':
- sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else ''
- sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))}
- quoted_body = '> %s' % tools.ustr(body.replace('\n', "\n> ") or '')
- body = '\n'.join(["\n", (sent_date + sender), quoted_body])
- body += "\n" + (current_user.signature or '')
- re_prefix = _("Re:")
- if not (subject.startswith('Re:') or subject.startswith(re_prefix)):
- subject = "%s %s" % (re_prefix, subject)
- result.update({
- 'subtype' : 'plain', # default to the text version due to quoting
- 'body_text' : body,
- 'subject' : subject,
- 'attachment_ids' : [],
- 'model' : message_data.model or False,
- 'res_id' : message_data.res_id or False,
- 'email_from' : current_user.user_email or message_data.email_to or False,
- 'email_to' : message_data.reply_to or message_data.email_from or False,
- 'email_cc' : message_data.email_cc or False,
- 'user_id' : uid,
+ if not message_id:
+ return result
- # pass msg-id and references of mail we're replying to, to construct the
- # new ones later when sending
- 'message_id' : message_data.message_id or False,
- 'references' : message_data.references and tools.ustr(message_data.references) or False,
- })
+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
+ message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context)
+ # Form the subject
+ re_prefix = _("Re:")
+ reply_subject = tools.ustr(message_data.subject or '')
+ if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)):
+ reply_subject = "%s %s" % (re_prefix, reply_subject)
+ # Form the bodies (text and html). We use the plain text version of the
+ # original mail, by default, as it is easier to quote than the HTML
+ # version. TODO: make it possible to switch to HTML on the fly
+ sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else ''
+ sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))}
+ body_text = message_data.body_text or ''
+ body_html = message_data.body_html or ''
+ quoted_body_text = '> %s' % tools.ustr(body_text.replace('\n', "\n> ") or '')
+ quoted_body_html = '
Quantity
diff --git a/addons/point_of_sale/report/pos_order_report_view.xml b/addons/point_of_sale/report/pos_order_report_view.xml
index 255af0cdd30..00e0eb91f2f 100644
--- a/addons/point_of_sale/report/pos_order_report_view.xml
+++ b/addons/point_of_sale/report/pos_order_report_view.xml
@@ -35,7 +35,14 @@
-
+
+
+
+
+
+
- Point of Sale Analysis
+ Orders Analysisreport.pos.orderformtree
- {'search_default_year':1,'search_default_This Month':1,'search_default_today':1,'search_default_User':1,'group_by_no_leaf':1,'group_by':[]}
+ {'search_default_year':1,'search_default_today':1,'group_by_no_leaf':1,'group_by':['product_id']}
diff --git a/addons/point_of_sale/report/pos_users_product.py.WORK b/addons/point_of_sale/report/pos_users_product.py.WORK
index f26be9c7172..51ab021e650 100644
--- a/addons/point_of_sale/report/pos_users_product.py.WORK
+++ b/addons/point_of_sale/report/pos_users_product.py.WORK
@@ -39,16 +39,13 @@ class pos_user_product(report_sxw.rml_parse):
data={}
for o in object :
sql1=""" SELECT distinct(o.id) from account_bank_statement s, account_bank_statement_line l,pos_order o,pos_order_line i where i.order_id=o.id and o.state in ('paid','invoiced') and l.statement_id=s.id and l.pos_statement_id=o.id and s.id=%d"""%(o.id)
- print sql1
self.cr.execute(sql1)
data = self.cr.dictfetchall()
- print "DATA",data
a_l=[]
for r in data:
if r['id']:
a_l.append(r['id'])
a = ','.join(map(str,a_l))
- print a_l, a
if len(a_l):
sql2="""SELECT sum(qty) as qty,l.price_unit*sum(l.qty) as amt,t.name as name from product_product p, product_template t, pos_order_line l where order_id in (%s) and p.product_tmpl_id=t.id and l.product_id=p.id group by t.name, l.price_unit"""%(a)
self.cr.execute(sql2)
diff --git a/addons/point_of_sale/report/report_cash_register.py b/addons/point_of_sale/report/report_cash_register.py
deleted file mode 100644
index 4e99d2010b1..00000000000
--- a/addons/point_of_sale/report/report_cash_register.py
+++ /dev/null
@@ -1,68 +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 tools
-from osv import fields,osv
-
-class report_cash_register(osv.osv):
- _name = "report.cash.register"
- _description = "Point of Sale Cash Register Analysis"
- _auto = False
- _columns = {
- 'date': fields.date('Create Date', readonly=True),
- 'year': fields.char('Year', size=4),
- 'month':fields.selection([('01','January'), ('02','February'), ('03','March'), ('04','April'),
- ('05','May'), ('06','June'), ('07','July'), ('08','August'), ('09','September'),
- ('10','October'), ('11','November'), ('12','December')], 'Month',readonly=True),
- 'day': fields.char('Day', size=128, readonly=True),
- 'user_id':fields.many2one('res.users', 'User', readonly=True),
- 'state': fields.selection([('draft', 'Quotation'),('open','Open'),('confirm', 'Confirmed')],'Status'),
- 'journal_id': fields.many2one('account.journal', 'Journal'),
- 'balance_start': fields.float('Opening Balance'),
- 'balance_end_real': fields.float('Closing Balance'),
- }
- _order = 'date desc'
-
- def init(self, cr):
- tools.drop_view_if_exists(cr, 'report_cash_register')
- cr.execute("""
- create or replace view report_cash_register as (
- select
- min(s.id) as id,
- to_date(to_char(s.create_date, 'dd-MM-YYYY'),'dd-MM-YYYY') as date,
- s.user_id as user_id,
- s.journal_id as journal_id,
- s.state as state,
- s.balance_start as balance_start,
- s.balance_end_real as balance_end_real,
- to_char(s.create_date, 'YYYY') as year,
- to_char(s.create_date, 'MM') as month,
- to_char(s.create_date, 'YYYY-MM-DD') as day
- from account_bank_statement as s
- group by
- s.user_id,s.journal_id, s.balance_start, s.balance_end_real,s.state,to_char(s.create_date, 'dd-MM-YYYY'),
- to_char(s.create_date, 'YYYY'),
- to_char(s.create_date, 'MM'),
- to_char(s.create_date, 'YYYY-MM-DD'))""")
-
-report_cash_register()
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/point_of_sale/report/report_cash_register_view.xml b/addons/point_of_sale/report/report_cash_register_view.xml
deleted file mode 100644
index afd769692df..00000000000
--- a/addons/point_of_sale/report/report_cash_register_view.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
- report.cash.register.tree
- report.cash.register
- tree
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- report.cash.register.search
- report.cash.register
- search
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Register Analysis
- report.cash.register
- form
- tree,form
-
- {'search_default_year':1,'search_default_This Month':1,'search_default_today':1,'search_default_User':1,'group_by_no_leaf':1,'group_by':[]}
-
-
-
-
-
diff --git a/addons/point_of_sale/report/user_label.xml b/addons/point_of_sale/report/user_label.xml
new file mode 100644
index 00000000000..60c4f950d76
--- /dev/null
+++ b/addons/point_of_sale/report/user_label.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/addons/point_of_sale/report/user_label.xsl b/addons/point_of_sale/report/user_label.xsl
new file mode 100644
index 00000000000..321cd830a7a
--- /dev/null
+++ b/addons/point_of_sale/report/user_label.xsl
@@ -0,0 +1,91 @@
+
+
+ 24.5
+ 0.5
+ 4.8
+ 10
+ 3cm
+ 9.3cm
+ 2
+ 16
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ cm
+
+
+
+ cm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/point_of_sale/res_users.py b/addons/point_of_sale/res_users.py
new file mode 100644
index 00000000000..d64f05d1391
--- /dev/null
+++ b/addons/point_of_sale/res_users.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+from osv import osv, fields
+import math
+
+def is_pair(x):
+ return not x%2
+# This code is a duplicate of product#check_ean function
+def check_ean(eancode):
+ if not eancode:
+ return True
+ if len(eancode) <> 13:
+ return False
+ try:
+ int(eancode)
+ except:
+ return False
+ oddsum=0
+ evensum=0
+ total=0
+ eanvalue=eancode
+ reversevalue = eanvalue[::-1]
+ finalean=reversevalue[1:]
+
+ for i in range(len(finalean)):
+ if is_pair(i):
+ oddsum += int(finalean[i])
+ else:
+ evensum += int(finalean[i])
+ total=(oddsum * 3) + evensum
+
+ check = int(10 - math.ceil(total % 10.0)) %10
+
+ if check != int(eancode[-1]):
+ return False
+ return True
+
+class res_users(osv.osv):
+ _inherit = 'res.users'
+ _columns = {
+ 'ean13' : fields.char('EAN13', size=13, help="BarCode"),
+ 'pos_config' : fields.many2one('pos.config', 'Default Point of Sale', domain=[('state', '=', 'active')]),
+ }
+
+ def _check_ean(self, cr, uid, ids, context=None):
+ return all(
+ check_ean(user.ean13) == True
+ for user in self.browse(cr, uid, ids, context=context)
+ )
+
+ _constraints = [
+ (_check_ean, "Error: Invalid ean code", ['ean13'],),
+ ]
+
diff --git a/addons/point_of_sale/res_users_view.xml b/addons/point_of_sale/res_users_view.xml
new file mode 100644
index 00000000000..def4a578252
--- /dev/null
+++ b/addons/point_of_sale/res_users_view.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ res.users.form.view
+ res.users
+ form
+
+
+
+
+
+
+
+
+
+ res.users.form.view
+ res.users
+ form
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/point_of_sale/security/ir.model.access.csv b/addons/point_of_sale/security/ir.model.access.csv
index 9eb77e81244..06673fc3da8 100644
--- a/addons/point_of_sale/security/ir.model.access.csv
+++ b/addons/point_of_sale/security/ir.model.access.csv
@@ -1,11 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_pos_config_journal,pos.config.journal,model_pos_config_journal,group_pos_user,1,0,0,0
access_pos_order,pos.order,model_pos_order,group_pos_user,1,1,1,1
access_pos_order_line,pos.order.line,model_pos_order_line,group_pos_user,1,1,1,1
access_pos_order_manager,pos.order manager,model_pos_order,group_pos_manager,1,0,0,0
access_pos_order_line_manager,pos.order.line manager,model_pos_order_line,group_pos_manager,1,0,0,0
access_report_transaction_pos,report.transaction.pos,model_report_transaction_pos,group_pos_manager,1,1,1,1
-access_pos_config_journal_manager,pos.config.journal.manager,model_pos_config_journal,group_pos_manager,1,1,1,1
access_account_journal_pos_user,account.journal pos_user,account.model_account_journal,group_pos_user,1,0,0,0
access_account_move_pos_user,account.move pos_user,account.model_account_move,group_pos_user,1,1,1,0
access_account_account_pos_user,account.account pos_user,account.model_account_account,group_pos_user,1,0,0,0
@@ -32,6 +30,7 @@ access_account_period_pos_manager,account.period pos manager,account.model_accou
access_account_fiscalyear_pos_user,account.fiscalyear user,account.model_account_fiscalyear,group_pos_user,1,1,1,0
access_account_fiscalyear_pos_manager,account.fiscalyear manager,account.model_account_fiscalyear,group_pos_manager,1,0,0,0
access_account_cashbox_line,account.cashbox.line,account.model_account_cashbox_line,group_pos_user,1,1,1,0
+access_account_journal_cashbox_line,account.cashbox.journal.line,account.model_account_journal_cashbox_line,group_pos_user,1,1,1,0
access_account_cashbox_line_manager,account.cashbox.line manager,account.model_account_cashbox_line,group_pos_manager,1,1,1,1
access_product_product,product.product,product.model_product_product,group_pos_user,1,0,0,0
access_product_template_pos_user,product.template pos user,product.model_product_template,group_pos_user,1,0,0,0
@@ -49,7 +48,6 @@ access_account_analytic_line,account.analytic.line,analytic.model_account_analyt
access_account_analytic_account,account.analytic.account,analytic.model_account_analytic_account,group_pos_user,1,1,1,0
access_account_journal_column,account.journal.column,account.model_account_journal_column,group_pos_user,1,1,1,0
access_account_journal_column_manager,account.journal.column manager,account.model_account_journal_column,group_pos_manager,1,0,0,0
-access_report_check_register,report.cash.register,model_report_cash_register,group_pos_manager,1,1,1,1
access_ir_property_pos_manager,ir.property manager,base.model_ir_property,group_pos_manager,1,1,1,1
access_account_bank_statement_line_manager,account.bank.statement.line manager,account.model_account_bank_statement_line,group_pos_manager,1,1,1,1
access_account_invoice_manager,account.invoice manager,account.model_account_invoice,group_pos_manager,1,1,1,1
@@ -64,3 +62,5 @@ access_product_category_manager,product.category manager,product.model_product_c
access_product_pricelist_manager,product.pricelist manager,product.model_product_pricelist,group_pos_manager,1,0,0,0
access_product_category_pos_manager,pos.category manager,model_pos_category,group_pos_manager,1,1,1,"1"""
access_product_category_pos_user,pos.category user,model_pos_category,group_pos_user,1,0,0,"0"""
+access_pos_session_user,pos.session user,model_pos_session,group_pos_user,1,1,1,0
+access_pos_config_user,pos.config user,model_pos_config,group_pos_user,1,1,1,0
diff --git a/addons/point_of_sale/static/lib/backbone/backbone-0.9.2.js b/addons/point_of_sale/static/lib/backbone/backbone-0.9.2.js
new file mode 100644
index 00000000000..3373c952bfa
--- /dev/null
+++ b/addons/point_of_sale/static/lib/backbone/backbone-0.9.2.js
@@ -0,0 +1,1431 @@
+// Backbone.js 0.9.2
+
+// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://backbonejs.org
+
+(function(){
+
+ // Initial Setup
+ // -------------
+
+ // Save a reference to the global object (`window` in the browser, `global`
+ // on the server).
+ var root = this;
+
+ // Save the previous value of the `Backbone` variable, so that it can be
+ // restored later on, if `noConflict` is used.
+ var previousBackbone = root.Backbone;
+
+ // Create a local reference to slice/splice.
+ var slice = Array.prototype.slice;
+ var splice = Array.prototype.splice;
+
+ // The top-level namespace. All public Backbone classes and modules will
+ // be attached to this. Exported for both CommonJS and the browser.
+ var Backbone;
+ if (typeof exports !== 'undefined') {
+ Backbone = exports;
+ } else {
+ Backbone = root.Backbone = {};
+ }
+
+ // Current version of the library. Keep in sync with `package.json`.
+ Backbone.VERSION = '0.9.2';
+
+ // Require Underscore, if we're on the server, and it's not already present.
+ var _ = root._;
+ if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
+
+ // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
+ var $ = root.jQuery || root.Zepto || root.ender;
+
+ // Set the JavaScript library that will be used for DOM manipulation and
+ // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
+ // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
+ // alternate JavaScript library (or a mock library for testing your views
+ // outside of a browser).
+ Backbone.setDomLibrary = function(lib) {
+ $ = lib;
+ };
+
+ // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+ // to its previous owner. Returns a reference to this Backbone object.
+ Backbone.noConflict = function() {
+ root.Backbone = previousBackbone;
+ return this;
+ };
+
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
+ // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
+ // set a `X-Http-Method-Override` header.
+ Backbone.emulateHTTP = false;
+
+ // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+ // `application/json` requests ... will encode the body as
+ // `application/x-www-form-urlencoded` instead and will send the model in a
+ // form param named `model`.
+ Backbone.emulateJSON = false;
+
+ // Backbone.Events
+ // -----------------
+
+ // Regular expression used to split event strings
+ var eventSplitter = /\s+/;
+
+ // A module that can be mixed in to *any object* in order to provide it with
+ // custom events. You may bind with `on` or remove with `off` callback functions
+ // to an event; trigger`-ing an event fires all callbacks in succession.
+ //
+ // var object = {};
+ // _.extend(object, Backbone.Events);
+ // object.on('expand', function(){ alert('expanded'); });
+ // object.trigger('expand');
+ //
+ var Events = Backbone.Events = {
+
+ // Bind one or more space separated events, `events`, to a `callback`
+ // function. Passing `"all"` will bind the callback to all events fired.
+ on: function(events, callback, context) {
+
+ var calls, event, node, tail, list;
+ if (!callback) return this;
+ events = events.split(eventSplitter);
+ calls = this._callbacks || (this._callbacks = {});
+
+ // Create an immutable callback list, allowing traversal during
+ // modification. The tail is an empty object that will always be used
+ // as the next node.
+ while (event = events.shift()) {
+ list = calls[event];
+ node = list ? list.tail : {};
+ node.next = tail = {};
+ node.context = context;
+ node.callback = callback;
+ calls[event] = {tail: tail, next: list ? list.next : node};
+ }
+
+ return this;
+ },
+
+ // Remove one or many callbacks. If `context` is null, removes all callbacks
+ // with that function. If `callback` is null, removes all callbacks for the
+ // event. If `events` is null, removes all bound callbacks for all events.
+ off: function(events, callback, context) {
+ var event, calls, node, tail, cb, ctx;
+
+ // No events, or removing *all* events.
+ if (!(calls = this._callbacks)) return;
+ if (!(events || callback || context)) {
+ delete this._callbacks;
+ return this;
+ }
+
+ // Loop through the listed events and contexts, splicing them out of the
+ // linked list of callbacks if appropriate.
+ events = events ? events.split(eventSplitter) : _.keys(calls);
+ while (event = events.shift()) {
+ node = calls[event];
+ delete calls[event];
+ if (!node || !(callback || context)) continue;
+ // Create a new list, omitting the indicated callbacks.
+ tail = node.tail;
+ while ((node = node.next) !== tail) {
+ cb = node.callback;
+ ctx = node.context;
+ if ((callback && cb !== callback) || (context && ctx !== context)) {
+ this.on(event, cb, ctx);
+ }
+ }
+ }
+
+ return this;
+ },
+
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
+ // passed the same arguments as `trigger` is, apart from the event name
+ // (unless you're listening on `"all"`, which will cause your callback to
+ // receive the true name of the event as the first argument).
+ trigger: function(events) {
+ var event, node, calls, tail, args, all, rest;
+ if (!(calls = this._callbacks)) return this;
+ all = calls.all;
+ events = events.split(eventSplitter);
+ rest = slice.call(arguments, 1);
+
+ // For each event, walk through the linked list of callbacks twice,
+ // first to trigger the event, then to trigger any `"all"` callbacks.
+ while (event = events.shift()) {
+ if (node = calls[event]) {
+ tail = node.tail;
+ while ((node = node.next) !== tail) {
+ node.callback.apply(node.context || this, rest);
+ }
+ }
+ if (node = all) {
+ tail = node.tail;
+ args = [event].concat(rest);
+ while ((node = node.next) !== tail) {
+ node.callback.apply(node.context || this, args);
+ }
+ }
+ }
+
+ return this;
+ }
+
+ };
+
+ // Aliases for backwards compatibility.
+ Events.bind = Events.on;
+ Events.unbind = Events.off;
+
+ // Backbone.Model
+ // --------------
+
+ // Create a new model, with defined attributes. A client id (`cid`)
+ // is automatically generated and assigned for you.
+ var Model = Backbone.Model = function(attributes, options) {
+ var defaults;
+ attributes || (attributes = {});
+ if (options && options.parse) attributes = this.parse(attributes);
+ if (defaults = getValue(this, 'defaults')) {
+ attributes = _.extend({}, defaults, attributes);
+ }
+ if (options && options.collection) this.collection = options.collection;
+ this.attributes = {};
+ this._escapedAttributes = {};
+ this.cid = _.uniqueId('c');
+ this.changed = {};
+ this._silent = {};
+ this._pending = {};
+ this.set(attributes, {silent: true});
+ // Reset change tracking.
+ this.changed = {};
+ this._silent = {};
+ this._pending = {};
+ this._previousAttributes = _.clone(this.attributes);
+ this.initialize.apply(this, arguments);
+ };
+
+ // Attach all inheritable methods to the Model prototype.
+ _.extend(Model.prototype, Events, {
+
+ // A hash of attributes whose current and previous value differ.
+ changed: null,
+
+ // A hash of attributes that have silently changed since the last time
+ // `change` was called. Will become pending attributes on the next call.
+ _silent: null,
+
+ // A hash of attributes that have changed since the last `'change'` event
+ // began.
+ _pending: null,
+
+ // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+ // CouchDB users may want to set this to `"_id"`.
+ idAttribute: 'id',
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // Return a copy of the model's `attributes` object.
+ toJSON: function(options) {
+ return _.clone(this.attributes);
+ },
+
+ // Get the value of an attribute.
+ get: function(attr) {
+ return this.attributes[attr];
+ },
+
+ // Get the HTML-escaped value of an attribute.
+ escape: function(attr) {
+ var html;
+ if (html = this._escapedAttributes[attr]) return html;
+ var val = this.get(attr);
+ return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
+ },
+
+ // Returns `true` if the attribute contains a value that is not null
+ // or undefined.
+ has: function(attr) {
+ return this.get(attr) != null;
+ },
+
+ // Set a hash of model attributes on the object, firing `"change"` unless
+ // you choose to silence it.
+ set: function(key, value, options) {
+ var attrs, attr, val;
+
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ if (_.isObject(key) || key == null) {
+ attrs = key;
+ options = value;
+ } else {
+ attrs = {};
+ attrs[key] = value;
+ }
+
+ // Extract attributes and options.
+ options || (options = {});
+ if (!attrs) return this;
+ if (attrs instanceof Model) attrs = attrs.attributes;
+ if (options.unset) for (attr in attrs) attrs[attr] = void 0;
+
+ // Run validation.
+ if (!this._validate(attrs, options)) return false;
+
+ // Check for changes of `id`.
+ if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+ var changes = options.changes = {};
+ var now = this.attributes;
+ var escaped = this._escapedAttributes;
+ var prev = this._previousAttributes || {};
+
+ // For each `set` attribute...
+ for (attr in attrs) {
+ val = attrs[attr];
+
+ // If the new and current value differ, record the change.
+ if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
+ delete escaped[attr];
+ (options.silent ? this._silent : changes)[attr] = true;
+ }
+
+ // Update or delete the current value.
+ options.unset ? delete now[attr] : now[attr] = val;
+
+ // If the new and previous value differ, record the change. If not,
+ // then remove changes for this attribute.
+ if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
+ this.changed[attr] = val;
+ if (!options.silent) this._pending[attr] = true;
+ } else {
+ delete this.changed[attr];
+ delete this._pending[attr];
+ }
+ }
+
+ // Fire the `"change"` events.
+ if (!options.silent) this.change(options);
+ return this;
+ },
+
+ // Remove an attribute from the model, firing `"change"` unless you choose
+ // to silence it. `unset` is a noop if the attribute doesn't exist.
+ unset: function(attr, options) {
+ (options || (options = {})).unset = true;
+ return this.set(attr, null, options);
+ },
+
+ // Clear all attributes on the model, firing `"change"` unless you choose
+ // to silence it.
+ clear: function(options) {
+ (options || (options = {})).unset = true;
+ return this.set(_.clone(this.attributes), options);
+ },
+
+ // Fetch the model from the server. If the server's representation of the
+ // model differs from its current attributes, they will be overriden,
+ // triggering a `"change"` event.
+ fetch: function(options) {
+ options = options ? _.clone(options) : {};
+ var model = this;
+ var success = options.success;
+ options.success = function(resp, status, xhr) {
+ if (!model.set(model.parse(resp, xhr), options)) return false;
+ if (success) success(model, resp);
+ };
+ options.error = Backbone.wrapError(options.error, model, options);
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);
+ },
+
+ // Set a hash of model attributes, and sync the model to the server.
+ // If the server returns an attributes hash that differs, the model's
+ // state will be `set` again.
+ save: function(key, value, options) {
+ var attrs, current;
+
+ // Handle both `("key", value)` and `({key: value})` -style calls.
+ if (_.isObject(key) || key == null) {
+ attrs = key;
+ options = value;
+ } else {
+ attrs = {};
+ attrs[key] = value;
+ }
+ options = options ? _.clone(options) : {};
+
+ // If we're "wait"-ing to set changed attributes, validate early.
+ if (options.wait) {
+ if (!this._validate(attrs, options)) return false;
+ current = _.clone(this.attributes);
+ }
+
+ // Regular saves `set` attributes before persisting to the server.
+ var silentOptions = _.extend({}, options, {silent: true});
+ if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
+ return false;
+ }
+
+ // After a successful server-side save, the client is (optionally)
+ // updated with the server-side state.
+ var model = this;
+ var success = options.success;
+ options.success = function(resp, status, xhr) {
+ var serverAttrs = model.parse(resp, xhr);
+ if (options.wait) {
+ delete options.wait;
+ serverAttrs = _.extend(attrs || {}, serverAttrs);
+ }
+ if (!model.set(serverAttrs, options)) return false;
+ if (success) {
+ success(model, resp);
+ } else {
+ model.trigger('sync', model, resp, options);
+ }
+ };
+
+ // Finish configuring and sending the Ajax request.
+ options.error = Backbone.wrapError(options.error, model, options);
+ var method = this.isNew() ? 'create' : 'update';
+ var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
+ if (options.wait) this.set(current, silentOptions);
+ return xhr;
+ },
+
+ // Destroy this model on the server if it was already persisted.
+ // Optimistically removes the model from its collection, if it has one.
+ // If `wait: true` is passed, waits for the server to respond before removal.
+ destroy: function(options) {
+ options = options ? _.clone(options) : {};
+ var model = this;
+ var success = options.success;
+
+ var triggerDestroy = function() {
+ model.trigger('destroy', model, model.collection, options);
+ };
+
+ if (this.isNew()) {
+ triggerDestroy();
+ return false;
+ }
+
+ options.success = function(resp) {
+ if (options.wait) triggerDestroy();
+ if (success) {
+ success(model, resp);
+ } else {
+ model.trigger('sync', model, resp, options);
+ }
+ };
+
+ options.error = Backbone.wrapError(options.error, model, options);
+ var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
+ if (!options.wait) triggerDestroy();
+ return xhr;
+ },
+
+ // Default URL for the model's representation on the server -- if you're
+ // using Backbone's restful methods, override this to change the endpoint
+ // that will be called.
+ url: function() {
+ var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
+ if (this.isNew()) return base;
+ return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
+ },
+
+ // **parse** converts a response into the hash of attributes to be `set` on
+ // the model. The default implementation is just to pass the response along.
+ parse: function(resp, xhr) {
+ return resp;
+ },
+
+ // Create a new model with identical attributes to this one.
+ clone: function() {
+ return new this.constructor(this.attributes);
+ },
+
+ // A model is new if it has never been saved to the server, and lacks an id.
+ isNew: function() {
+ return this.id == null;
+ },
+
+ // Call this method to manually fire a `"change"` event for this model and
+ // a `"change:attribute"` event for each changed attribute.
+ // Calling this will cause all objects observing the model to update.
+ change: function(options) {
+ options || (options = {});
+ var changing = this._changing;
+ this._changing = true;
+
+ // Silent changes become pending changes.
+ for (var attr in this._silent) this._pending[attr] = true;
+
+ // Silent changes are triggered.
+ var changes = _.extend({}, options.changes, this._silent);
+ this._silent = {};
+ for (var attr in changes) {
+ this.trigger('change:' + attr, this, this.get(attr), options);
+ }
+ if (changing) return this;
+
+ // Continue firing `"change"` events while there are pending changes.
+ while (!_.isEmpty(this._pending)) {
+ this._pending = {};
+ this.trigger('change', this, options);
+ // Pending and silent changes still remain.
+ for (var attr in this.changed) {
+ if (this._pending[attr] || this._silent[attr]) continue;
+ delete this.changed[attr];
+ }
+ this._previousAttributes = _.clone(this.attributes);
+ }
+
+ this._changing = false;
+ return this;
+ },
+
+ // Determine if the model has changed since the last `"change"` event.
+ // If you specify an attribute name, determine if that attribute has changed.
+ hasChanged: function(attr) {
+ if (!arguments.length) return !_.isEmpty(this.changed);
+ return _.has(this.changed, attr);
+ },
+
+ // Return an object containing all the attributes that have changed, or
+ // false if there are no changed attributes. Useful for determining what
+ // parts of a view need to be updated and/or what attributes need to be
+ // persisted to the server. Unset attributes will be set to undefined.
+ // You can also pass an attributes object to diff against the model,
+ // determining if there *would be* a change.
+ changedAttributes: function(diff) {
+ if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
+ var val, changed = false, old = this._previousAttributes;
+ for (var attr in diff) {
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+ (changed || (changed = {}))[attr] = val;
+ }
+ return changed;
+ },
+
+ // Get the previous value of an attribute, recorded at the time the last
+ // `"change"` event was fired.
+ previous: function(attr) {
+ if (!arguments.length || !this._previousAttributes) return null;
+ return this._previousAttributes[attr];
+ },
+
+ // Get all of the attributes of the model at the time of the previous
+ // `"change"` event.
+ previousAttributes: function() {
+ return _.clone(this._previousAttributes);
+ },
+
+ // Check if the model is currently in a valid state. It's only possible to
+ // get into an *invalid* state if you're using silent changes.
+ isValid: function() {
+ return !this.validate(this.attributes);
+ },
+
+ // Run validation against the next complete set of model attributes,
+ // returning `true` if all is well. If a specific `error` callback has
+ // been passed, call that instead of firing the general `"error"` event.
+ _validate: function(attrs, options) {
+ if (options.silent || !this.validate) return true;
+ attrs = _.extend({}, this.attributes, attrs);
+ var error = this.validate(attrs, options);
+ if (!error) return true;
+ if (options && options.error) {
+ options.error(this, error, options);
+ } else {
+ this.trigger('error', this, error, options);
+ }
+ return false;
+ }
+
+ });
+
+ // Backbone.Collection
+ // -------------------
+
+ // Provides a standard collection class for our sets of models, ordered
+ // or unordered. If a `comparator` is specified, the Collection will maintain
+ // its models in sort order, as they're added and removed.
+ var Collection = Backbone.Collection = function(models, options) {
+ options || (options = {});
+ if (options.model) this.model = options.model;
+ if (options.comparator) this.comparator = options.comparator;
+ this._reset();
+ this.initialize.apply(this, arguments);
+ if (models) this.reset(models, {silent: true, parse: options.parse});
+ };
+
+ // Define the Collection's inheritable methods.
+ _.extend(Collection.prototype, Events, {
+
+ // The default model for a collection is just a **Backbone.Model**.
+ // This should be overridden in most cases.
+ model: Model,
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // The JSON representation of a Collection is an array of the
+ // models' attributes.
+ toJSON: function(options) {
+ return this.map(function(model){ return model.toJSON(options); });
+ },
+
+ // Add a model, or list of models to the set. Pass **silent** to avoid
+ // firing the `add` event for every new model.
+ add: function(models, options) {
+ var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
+ options || (options = {});
+ models = _.isArray(models) ? models.slice() : [models];
+
+ // Begin by turning bare objects into model references, and preventing
+ // invalid models or duplicate models from being added.
+ for (i = 0, length = models.length; i < length; i++) {
+ if (!(model = models[i] = this._prepareModel(models[i], options))) {
+ throw new Error("Can't add an invalid model to a collection");
+ }
+ cid = model.cid;
+ id = model.id;
+ if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
+ dups.push(i);
+ continue;
+ }
+ cids[cid] = ids[id] = model;
+ }
+
+ // Remove duplicates.
+ i = dups.length;
+ while (i--) {
+ models.splice(dups[i], 1);
+ }
+
+ // Listen to added models' events, and index models for lookup by
+ // `id` and by `cid`.
+ for (i = 0, length = models.length; i < length; i++) {
+ (model = models[i]).on('all', this._onModelEvent, this);
+ this._byCid[model.cid] = model;
+ if (model.id != null) this._byId[model.id] = model;
+ }
+
+ // Insert models into the collection, re-sorting if needed, and triggering
+ // `add` events unless silenced.
+ this.length += length;
+ index = options.at != null ? options.at : this.models.length;
+ splice.apply(this.models, [index, 0].concat(models));
+ if (this.comparator) this.sort({silent: true});
+ if (options.silent) return this;
+ for (i = 0, length = this.models.length; i < length; i++) {
+ if (!cids[(model = this.models[i]).cid]) continue;
+ options.index = i;
+ model.trigger('add', model, this, options);
+ }
+ return this;
+ },
+
+ // Remove a model, or a list of models from the set. Pass silent to avoid
+ // firing the `remove` event for every model removed.
+ remove: function(models, options) {
+ var i, l, index, model;
+ options || (options = {});
+ models = _.isArray(models) ? models.slice() : [models];
+ for (i = 0, l = models.length; i < l; i++) {
+ model = this.getByCid(models[i]) || this.get(models[i]);
+ if (!model) continue;
+ delete this._byId[model.id];
+ delete this._byCid[model.cid];
+ index = this.indexOf(model);
+ this.models.splice(index, 1);
+ this.length--;
+ if (!options.silent) {
+ options.index = index;
+ model.trigger('remove', model, this, options);
+ }
+ this._removeReference(model);
+ }
+ return this;
+ },
+
+ // Add a model to the end of the collection.
+ push: function(model, options) {
+ model = this._prepareModel(model, options);
+ this.add(model, options);
+ return model;
+ },
+
+ // Remove a model from the end of the collection.
+ pop: function(options) {
+ var model = this.at(this.length - 1);
+ this.remove(model, options);
+ return model;
+ },
+
+ // Add a model to the beginning of the collection.
+ unshift: function(model, options) {
+ model = this._prepareModel(model, options);
+ this.add(model, _.extend({at: 0}, options));
+ return model;
+ },
+
+ // Remove a model from the beginning of the collection.
+ shift: function(options) {
+ var model = this.at(0);
+ this.remove(model, options);
+ return model;
+ },
+
+ // Get a model from the set by id.
+ get: function(id) {
+ if (id == null) return void 0;
+ return this._byId[id.id != null ? id.id : id];
+ },
+
+ // Get a model from the set by client id.
+ getByCid: function(cid) {
+ return cid && this._byCid[cid.cid || cid];
+ },
+
+ // Get the model at the given index.
+ at: function(index) {
+ return this.models[index];
+ },
+
+ // Return models with matching attributes. Useful for simple cases of `filter`.
+ where: function(attrs) {
+ if (_.isEmpty(attrs)) return [];
+ return this.filter(function(model) {
+ for (var key in attrs) {
+ if (attrs[key] !== model.get(key)) return false;
+ }
+ return true;
+ });
+ },
+
+ // Force the collection to re-sort itself. You don't need to call this under
+ // normal circumstances, as the set will maintain sort order as each item
+ // is added.
+ sort: function(options) {
+ options || (options = {});
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+ var boundComparator = _.bind(this.comparator, this);
+ if (this.comparator.length == 1) {
+ this.models = this.sortBy(boundComparator);
+ } else {
+ this.models.sort(boundComparator);
+ }
+ if (!options.silent) this.trigger('reset', this, options);
+ return this;
+ },
+
+ // Pluck an attribute from each model in the collection.
+ pluck: function(attr) {
+ return _.map(this.models, function(model){ return model.get(attr); });
+ },
+
+ // When you have more items than you want to add or remove individually,
+ // you can reset the entire set with a new list of models, without firing
+ // any `add` or `remove` events. Fires `reset` when finished.
+ reset: function(models, options) {
+ models || (models = []);
+ options || (options = {});
+ for (var i = 0, l = this.models.length; i < l; i++) {
+ this._removeReference(this.models[i]);
+ }
+ this._reset();
+ this.add(models, _.extend({silent: true}, options));
+ if (!options.silent) this.trigger('reset', this, options);
+ return this;
+ },
+
+ // Fetch the default set of models for this collection, resetting the
+ // collection when they arrive. If `add: true` is passed, appends the
+ // models to the collection instead of resetting.
+ fetch: function(options) {
+ options = options ? _.clone(options) : {};
+ if (options.parse === undefined) options.parse = true;
+ var collection = this;
+ var success = options.success;
+ options.success = function(resp, status, xhr) {
+ collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
+ if (success) success(collection, resp);
+ };
+ options.error = Backbone.wrapError(options.error, collection, options);
+ return (this.sync || Backbone.sync).call(this, 'read', this, options);
+ },
+
+ // Create a new instance of a model in this collection. Add the model to the
+ // collection immediately, unless `wait: true` is passed, in which case we
+ // wait for the server to agree.
+ create: function(model, options) {
+ var coll = this;
+ options = options ? _.clone(options) : {};
+ model = this._prepareModel(model, options);
+ if (!model) return false;
+ if (!options.wait) coll.add(model, options);
+ var success = options.success;
+ options.success = function(nextModel, resp, xhr) {
+ if (options.wait) coll.add(nextModel, options);
+ if (success) {
+ success(nextModel, resp);
+ } else {
+ nextModel.trigger('sync', model, resp, options);
+ }
+ };
+ model.save(null, options);
+ return model;
+ },
+
+ // **parse** converts a response into a list of models to be added to the
+ // collection. The default implementation is just to pass it through.
+ parse: function(resp, xhr) {
+ return resp;
+ },
+
+ // Proxy to _'s chain. Can't be proxied the same way the rest of the
+ // underscore methods are proxied because it relies on the underscore
+ // constructor.
+ chain: function () {
+ return _(this.models).chain();
+ },
+
+ // Reset all internal state. Called when the collection is reset.
+ _reset: function(options) {
+ this.length = 0;
+ this.models = [];
+ this._byId = {};
+ this._byCid = {};
+ },
+
+ // Prepare a model or hash of attributes to be added to this collection.
+ _prepareModel: function(model, options) {
+ options || (options = {});
+ if (!(model instanceof Model)) {
+ var attrs = model;
+ options.collection = this;
+ model = new this.model(attrs, options);
+ if (!model._validate(model.attributes, options)) model = false;
+ } else if (!model.collection) {
+ model.collection = this;
+ }
+ return model;
+ },
+
+ // Internal method to remove a model's ties to a collection.
+ _removeReference: function(model) {
+ if (this == model.collection) {
+ delete model.collection;
+ }
+ model.off('all', this._onModelEvent, this);
+ },
+
+ // Internal method called every time a model in the set fires an event.
+ // Sets need to update their indexes when models change ids. All other
+ // events simply proxy through. "add" and "remove" events that originate
+ // in other collections are ignored.
+ _onModelEvent: function(event, model, collection, options) {
+ if ((event == 'add' || event == 'remove') && collection != this) return;
+ if (event == 'destroy') {
+ this.remove(model, options);
+ }
+ if (model && event === 'change:' + model.idAttribute) {
+ delete this._byId[model.previous(model.idAttribute)];
+ this._byId[model.id] = model;
+ }
+ this.trigger.apply(this, arguments);
+ }
+
+ });
+
+ // Underscore methods that we want to implement on the Collection.
+ var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
+ 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
+ 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
+ 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
+ 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
+
+ // Mix in each Underscore method as a proxy to `Collection#models`.
+ _.each(methods, function(method) {
+ Collection.prototype[method] = function() {
+ return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
+ };
+ });
+
+ // Backbone.Router
+ // -------------------
+
+ // Routers map faux-URLs to actions, and fire events when routes are
+ // matched. Creating a new one sets its `routes` hash, if not set statically.
+ var Router = Backbone.Router = function(options) {
+ options || (options = {});
+ if (options.routes) this.routes = options.routes;
+ this._bindRoutes();
+ this.initialize.apply(this, arguments);
+ };
+
+ // Cached regular expressions for matching named param parts and splatted
+ // parts of route strings.
+ var namedParam = /:\w+/g;
+ var splatParam = /\*\w+/g;
+ var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
+
+ // Set up all inheritable **Backbone.Router** properties and methods.
+ _.extend(Router.prototype, Events, {
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // Manually bind a single named route to a callback. For example:
+ //
+ // this.route('search/:query/p:num', 'search', function(query, num) {
+ // ...
+ // });
+ //
+ route: function(route, name, callback) {
+ Backbone.history || (Backbone.history = new History);
+ if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+ if (!callback) callback = this[name];
+ Backbone.history.route(route, _.bind(function(fragment) {
+ var args = this._extractParameters(route, fragment);
+ callback && callback.apply(this, args);
+ this.trigger.apply(this, ['route:' + name].concat(args));
+ Backbone.history.trigger('route', this, name, args);
+ }, this));
+ return this;
+ },
+
+ // Simple proxy to `Backbone.history` to save a fragment into the history.
+ navigate: function(fragment, options) {
+ Backbone.history.navigate(fragment, options);
+ },
+
+ // Bind all defined routes to `Backbone.history`. We have to reverse the
+ // order of the routes here to support behavior where the most general
+ // routes can be defined at the bottom of the route map.
+ _bindRoutes: function() {
+ if (!this.routes) return;
+ var routes = [];
+ for (var route in this.routes) {
+ routes.unshift([route, this.routes[route]]);
+ }
+ for (var i = 0, l = routes.length; i < l; i++) {
+ this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
+ }
+ },
+
+ // Convert a route string into a regular expression, suitable for matching
+ // against the current location hash.
+ _routeToRegExp: function(route) {
+ route = route.replace(escapeRegExp, '\\$&')
+ .replace(namedParam, '([^\/]+)')
+ .replace(splatParam, '(.*?)');
+ return new RegExp('^' + route + '$');
+ },
+
+ // Given a route, and a URL fragment that it matches, return the array of
+ // extracted parameters.
+ _extractParameters: function(route, fragment) {
+ return route.exec(fragment).slice(1);
+ }
+
+ });
+
+ // Backbone.History
+ // ----------------
+
+ // Handles cross-browser history management, based on URL fragments. If the
+ // browser does not support `onhashchange`, falls back to polling.
+ var History = Backbone.History = function() {
+ this.handlers = [];
+ _.bindAll(this, 'checkUrl');
+ };
+
+ // Cached regex for cleaning leading hashes and slashes .
+ var routeStripper = /^[#\/]/;
+
+ // Cached regex for detecting MSIE.
+ var isExplorer = /msie [\w.]+/;
+
+ // Has the history handling already been started?
+ History.started = false;
+
+ // Set up all inheritable **Backbone.History** properties and methods.
+ _.extend(History.prototype, Events, {
+
+ // The default interval to poll for hash changes, if necessary, is
+ // twenty times a second.
+ interval: 50,
+
+ // Gets the true hash value. Cannot use location.hash directly due to bug
+ // in Firefox where location.hash will always be decoded.
+ getHash: function(windowOverride) {
+ var loc = windowOverride ? windowOverride.location : window.location;
+ var match = loc.href.match(/#(.*)$/);
+ return match ? match[1] : '';
+ },
+
+ // Get the cross-browser normalized URL fragment, either from the URL,
+ // the hash, or the override.
+ getFragment: function(fragment, forcePushState) {
+ if (fragment == null) {
+ if (this._hasPushState || forcePushState) {
+ fragment = window.location.pathname;
+ var search = window.location.search;
+ if (search) fragment += search;
+ } else {
+ fragment = this.getHash();
+ }
+ }
+ if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
+ return fragment.replace(routeStripper, '');
+ },
+
+ // Start the hash change handling, returning `true` if the current URL matches
+ // an existing route, and `false` otherwise.
+ start: function(options) {
+ if (History.started) throw new Error("Backbone.history has already been started");
+ History.started = true;
+
+ // Figure out the initial configuration. Do we need an iframe?
+ // Is pushState desired ... is it available?
+ this.options = _.extend({}, {root: '/'}, this.options, options);
+ this._wantsHashChange = this.options.hashChange !== false;
+ this._wantsPushState = !!this.options.pushState;
+ this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
+ var fragment = this.getFragment();
+ var docMode = document.documentMode;
+ var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
+
+ if (oldIE) {
+ this.iframe = $('').hide().appendTo('body')[0].contentWindow;
+ this.navigate(fragment);
+ }
+
+ // Depending on whether we're using pushState or hashes, and whether
+ // 'onhashchange' is supported, determine how we check the URL state.
+ if (this._hasPushState) {
+ $(window).bind('popstate', this.checkUrl);
+ } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+ $(window).bind('hashchange', this.checkUrl);
+ } else if (this._wantsHashChange) {
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+ }
+
+ // Determine if we need to change the base url, for a pushState link
+ // opened by a non-pushState browser.
+ this.fragment = fragment;
+ var loc = window.location;
+ var atRoot = loc.pathname == this.options.root;
+
+ // If we've started off with a route from a `pushState`-enabled browser,
+ // but we're currently in a browser that doesn't support it...
+ if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
+ this.fragment = this.getFragment(null, true);
+ window.location.replace(this.options.root + '#' + this.fragment);
+ // Return immediately as browser will do redirect to new url
+ return true;
+
+ // Or if we've started out with a hash-based route, but we're currently
+ // in a browser where it could be `pushState`-based instead...
+ } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
+ this.fragment = this.getHash().replace(routeStripper, '');
+ window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
+ }
+
+ if (!this.options.silent) {
+ return this.loadUrl();
+ }
+ },
+
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+ // but possibly useful for unit testing Routers.
+ stop: function() {
+ $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
+ clearInterval(this._checkUrlInterval);
+ History.started = false;
+ },
+
+ // Add a route to be tested when the fragment changes. Routes added later
+ // may override previous routes.
+ route: function(route, callback) {
+ this.handlers.unshift({route: route, callback: callback});
+ },
+
+ // Checks the current URL to see if it has changed, and if it has,
+ // calls `loadUrl`, normalizing across the hidden iframe.
+ checkUrl: function(e) {
+ var current = this.getFragment();
+ if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
+ if (current == this.fragment) return false;
+ if (this.iframe) this.navigate(current);
+ this.loadUrl() || this.loadUrl(this.getHash());
+ },
+
+ // Attempt to load the current URL fragment. If a route succeeds with a
+ // match, returns `true`. If no defined routes matches the fragment,
+ // returns `false`.
+ loadUrl: function(fragmentOverride) {
+ var fragment = this.fragment = this.getFragment(fragmentOverride);
+ var matched = _.any(this.handlers, function(handler) {
+ if (handler.route.test(fragment)) {
+ handler.callback(fragment);
+ return true;
+ }
+ });
+ return matched;
+ },
+
+ // Save a fragment into the hash history, or replace the URL state if the
+ // 'replace' option is passed. You are responsible for properly URL-encoding
+ // the fragment in advance.
+ //
+ // The options object can contain `trigger: true` if you wish to have the
+ // route callback be fired (not usually desirable), or `replace: true`, if
+ // you wish to modify the current URL without adding an entry to the history.
+ navigate: function(fragment, options) {
+ if (!History.started) return false;
+ if (!options || options === true) options = {trigger: options};
+ var frag = (fragment || '').replace(routeStripper, '');
+ if (this.fragment == frag) return;
+
+ // If pushState is available, we use it to set the fragment as a real URL.
+ if (this._hasPushState) {
+ if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
+ this.fragment = frag;
+ window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
+
+ // If hash changes haven't been explicitly disabled, update the hash
+ // fragment to store history.
+ } else if (this._wantsHashChange) {
+ this.fragment = frag;
+ this._updateHash(window.location, frag, options.replace);
+ if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
+ // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
+ // When replace is true, we don't want this.
+ if(!options.replace) this.iframe.document.open().close();
+ this._updateHash(this.iframe.location, frag, options.replace);
+ }
+
+ // If you've told us that you explicitly don't want fallback hashchange-
+ // based history, then `navigate` becomes a page refresh.
+ } else {
+ window.location.assign(this.options.root + fragment);
+ }
+ if (options.trigger) this.loadUrl(fragment);
+ },
+
+ // Update the hash location, either replacing the current entry, or adding
+ // a new one to the browser history.
+ _updateHash: function(location, fragment, replace) {
+ if (replace) {
+ location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
+ } else {
+ location.hash = fragment;
+ }
+ }
+ });
+
+ // Backbone.View
+ // -------------
+
+ // Creating a Backbone.View creates its initial element outside of the DOM,
+ // if an existing element is not provided...
+ var View = Backbone.View = function(options) {
+ this.cid = _.uniqueId('view');
+ this._configure(options || {});
+ this._ensureElement();
+ this.initialize.apply(this, arguments);
+ this.delegateEvents();
+ };
+
+ // Cached regex to split keys for `delegate`.
+ var delegateEventSplitter = /^(\S+)\s*(.*)$/;
+
+ // List of view options to be merged as properties.
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
+
+ // Set up all inheritable **Backbone.View** properties and methods.
+ _.extend(View.prototype, Events, {
+
+ // The default `tagName` of a View's element is `"div"`.
+ tagName: 'div',
+
+ // jQuery delegate for element lookup, scoped to DOM elements within the
+ // current view. This should be prefered to global lookups where possible.
+ $: function(selector) {
+ return this.$el.find(selector);
+ },
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // **render** is the core function that your view should override, in order
+ // to populate its element (`this.el`), with the appropriate HTML. The
+ // convention is for **render** to always return `this`.
+ render: function() {
+ return this;
+ },
+
+ // Remove this view from the DOM. Note that the view isn't present in the
+ // DOM by default, so calling this method may be a no-op.
+ remove: function() {
+ this.$el.remove();
+ return this;
+ },
+
+ // For small amounts of DOM Elements, where a full-blown template isn't
+ // needed, use **make** to manufacture elements, one at a time.
+ //
+ // var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
+ //
+ make: function(tagName, attributes, content) {
+ var el = document.createElement(tagName);
+ if (attributes) $(el).attr(attributes);
+ if (content) $(el).html(content);
+ return el;
+ },
+
+ // Change the view's element (`this.el` property), including event
+ // re-delegation.
+ setElement: function(element, delegate) {
+ if (this.$el) this.undelegateEvents();
+ this.$el = (element instanceof $) ? element : $(element);
+ this.el = this.$el[0];
+ if (delegate !== false) this.delegateEvents();
+ return this;
+ },
+
+ // Set callbacks, where `this.events` is a hash of
+ //
+ // *{"event selector": "callback"}*
+ //
+ // {
+ // 'mousedown .title': 'edit',
+ // 'click .button': 'save'
+ // 'click .open': function(e) { ... }
+ // }
+ //
+ // pairs. Callbacks will be bound to the view, with `this` set properly.
+ // Uses event delegation for efficiency.
+ // Omitting the selector binds the event to `this.el`.
+ // This only works for delegate-able events: not `focus`, `blur`, and
+ // not `change`, `submit`, and `reset` in Internet Explorer.
+ delegateEvents: function(events) {
+ if (!(events || (events = getValue(this, 'events')))) return;
+ this.undelegateEvents();
+ for (var key in events) {
+ var method = events[key];
+ if (!_.isFunction(method)) method = this[events[key]];
+ if (!method) throw new Error('Method "' + events[key] + '" does not exist');
+ var match = key.match(delegateEventSplitter);
+ var eventName = match[1], selector = match[2];
+ method = _.bind(method, this);
+ eventName += '.delegateEvents' + this.cid;
+ if (selector === '') {
+ this.$el.bind(eventName, method);
+ } else {
+ this.$el.delegate(selector, eventName, method);
+ }
+ }
+ },
+
+ // Clears all callbacks previously bound to the view with `delegateEvents`.
+ // You usually don't need to use this, but may wish to if you have multiple
+ // Backbone views attached to the same DOM element.
+ undelegateEvents: function() {
+ this.$el.unbind('.delegateEvents' + this.cid);
+ },
+
+ // Performs the initial configuration of a View with a set of options.
+ // Keys with special meaning *(model, collection, id, className)*, are
+ // attached directly to the view.
+ _configure: function(options) {
+ if (this.options) options = _.extend({}, this.options, options);
+ for (var i = 0, l = viewOptions.length; i < l; i++) {
+ var attr = viewOptions[i];
+ if (options[attr]) this[attr] = options[attr];
+ }
+ this.options = options;
+ },
+
+ // Ensure that the View has a DOM element to render into.
+ // If `this.el` is a string, pass it through `$()`, take the first
+ // matching element, and re-assign it to `el`. Otherwise, create
+ // an element from the `id`, `className` and `tagName` properties.
+ _ensureElement: function() {
+ if (!this.el) {
+ var attrs = getValue(this, 'attributes') || {};
+ if (this.id) attrs.id = this.id;
+ if (this.className) attrs['class'] = this.className;
+ this.setElement(this.make(this.tagName, attrs), false);
+ } else {
+ this.setElement(this.el, false);
+ }
+ }
+
+ });
+
+ // The self-propagating extend function that Backbone classes use.
+ var extend = function (protoProps, classProps) {
+ var child = inherits(this, protoProps, classProps);
+ child.extend = this.extend;
+ return child;
+ };
+
+ // Set up inheritance for the model, collection, and view.
+ Model.extend = Collection.extend = Router.extend = View.extend = extend;
+
+ // Backbone.sync
+ // -------------
+
+ // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+ var methodMap = {
+ 'create': 'POST',
+ 'update': 'PUT',
+ 'delete': 'DELETE',
+ 'read': 'GET'
+ };
+
+ // Override this function to change the manner in which Backbone persists
+ // models to the server. You will be passed the type of request, and the
+ // model in question. By default, makes a RESTful Ajax request
+ // to the model's `url()`. Some possible customizations could be:
+ //
+ // * Use `setTimeout` to batch rapid-fire updates into a single request.
+ // * Send up the models as XML instead of JSON.
+ // * Persist models via WebSockets instead of Ajax.
+ //
+ // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+ // as `POST`, with a `_method` parameter containing the true HTTP method,
+ // as well as all requests with the body as `application/x-www-form-urlencoded`
+ // instead of `application/json` with the model in a param named `model`.
+ // Useful when interfacing with server-side languages like **PHP** that make
+ // it difficult to read the body of `PUT` requests.
+ Backbone.sync = function(method, model, options) {
+ var type = methodMap[method];
+
+ // Default options, unless specified.
+ options || (options = {});
+
+ // Default JSON-request options.
+ var params = {type: type, dataType: 'json'};
+
+ // Ensure that we have a URL.
+ if (!options.url) {
+ params.url = getValue(model, 'url') || urlError();
+ }
+
+ // Ensure that we have the appropriate request data.
+ if (!options.data && model && (method == 'create' || method == 'update')) {
+ params.contentType = 'application/json';
+ params.data = JSON.stringify(model.toJSON());
+ }
+
+ // For older servers, emulate JSON by encoding the request into an HTML-form.
+ if (Backbone.emulateJSON) {
+ params.contentType = 'application/x-www-form-urlencoded';
+ params.data = params.data ? {model: params.data} : {};
+ }
+
+ // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+ // And an `X-HTTP-Method-Override` header.
+ if (Backbone.emulateHTTP) {
+ if (type === 'PUT' || type === 'DELETE') {
+ if (Backbone.emulateJSON) params.data._method = type;
+ params.type = 'POST';
+ params.beforeSend = function(xhr) {
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
+ };
+ }
+ }
+
+ // Don't process data on a non-GET request.
+ if (params.type !== 'GET' && !Backbone.emulateJSON) {
+ params.processData = false;
+ }
+
+ // Make the request, allowing the user to override any Ajax options.
+ return $.ajax(_.extend(params, options));
+ };
+
+ // Wrap an optional error callback with a fallback error event.
+ Backbone.wrapError = function(onError, originalModel, options) {
+ return function(model, resp) {
+ resp = model === originalModel ? resp : model;
+ if (onError) {
+ onError(originalModel, resp, options);
+ } else {
+ originalModel.trigger('error', originalModel, resp, options);
+ }
+ };
+ };
+
+ // Helpers
+ // -------
+
+ // Shared empty constructor function to aid in prototype-chain creation.
+ var ctor = function(){};
+
+ // Helper function to correctly set up the prototype chain, for subclasses.
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
+ // class properties to be extended.
+ var inherits = function(parent, protoProps, staticProps) {
+ var child;
+
+ // The constructor function for the new subclass is either defined by you
+ // (the "constructor" property in your `extend` definition), or defaulted
+ // by us to simply call the parent's constructor.
+ if (protoProps && protoProps.hasOwnProperty('constructor')) {
+ child = protoProps.constructor;
+ } else {
+ child = function(){ parent.apply(this, arguments); };
+ }
+
+ // Inherit class (static) properties from parent.
+ _.extend(child, parent);
+
+ // Set the prototype chain to inherit from `parent`, without calling
+ // `parent`'s constructor function.
+ ctor.prototype = parent.prototype;
+ child.prototype = new ctor();
+
+ // Add prototype properties (instance properties) to the subclass,
+ // if supplied.
+ if (protoProps) _.extend(child.prototype, protoProps);
+
+ // Add static properties to the constructor function, if supplied.
+ if (staticProps) _.extend(child, staticProps);
+
+ // Correctly set child's `prototype.constructor`.
+ child.prototype.constructor = child;
+
+ // Set a convenience property in case the parent's prototype is needed later.
+ child.__super__ = parent.prototype;
+
+ return child;
+ };
+
+ // Helper function to get a value from a Backbone object as a property
+ // or as a function.
+ var getValue = function(object, prop) {
+ if (!(object && object[prop])) return null;
+ return _.isFunction(object[prop]) ? object[prop]() : object[prop];
+ };
+
+ // Throw an error when a URL is needed, and none is supplied.
+ var urlError = function() {
+ throw new Error('A "url" property or function must be specified');
+ };
+
+}).call(this);
diff --git a/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js b/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js
new file mode 100755
index 00000000000..38b60951b20
--- /dev/null
+++ b/addons/point_of_sale/static/lib/mousewheel/jquery.mousewheel-3.0.6.js
@@ -0,0 +1,84 @@
+/*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net)
+ * Licensed under the MIT License (LICENSE.txt).
+ *
+ * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
+ * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
+ * Thanks to: Seamus Leahy for adding deltaX and deltaY
+ *
+ * Version: 3.0.6
+ *
+ * Requires: 1.2.2+
+ */
+
+(function($) {
+
+var types = ['DOMMouseScroll', 'mousewheel'];
+
+if ($.event.fixHooks) {
+ for ( var i=types.length; i; ) {
+ $.event.fixHooks[ types[--i] ] = $.event.mouseHooks;
+ }
+}
+
+$.event.special.mousewheel = {
+ setup: function() {
+ if ( this.addEventListener ) {
+ for ( var i=types.length; i; ) {
+ this.addEventListener( types[--i], handler, false );
+ }
+ } else {
+ this.onmousewheel = handler;
+ }
+ },
+
+ teardown: function() {
+ if ( this.removeEventListener ) {
+ for ( var i=types.length; i; ) {
+ this.removeEventListener( types[--i], handler, false );
+ }
+ } else {
+ this.onmousewheel = null;
+ }
+ }
+};
+
+$.fn.extend({
+ mousewheel: function(fn) {
+ return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel");
+ },
+
+ unmousewheel: function(fn) {
+ return this.unbind("mousewheel", fn);
+ }
+});
+
+
+function handler(event) {
+ var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0;
+ event = $.event.fix(orgEvent);
+ event.type = "mousewheel";
+
+ // Old school scrollwheel delta
+ if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; }
+ if ( orgEvent.detail ) { delta = -orgEvent.detail/3; }
+
+ // New school multidimensional scroll (touchpads) deltas
+ deltaY = delta;
+
+ // Gecko
+ if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
+ deltaY = 0;
+ deltaX = -1*delta;
+ }
+
+ // Webkit
+ if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; }
+ if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; }
+
+ // Add event and delta to the front of the arguments
+ args.unshift(event, delta, deltaX, deltaY);
+
+ return ($.event.dispatch || $.event.handle).apply(this, args);
+}
+
+})(jQuery);
diff --git a/addons/point_of_sale/static/src/css/keyboard.css b/addons/point_of_sale/static/src/css/keyboard.css
new file mode 100644
index 00000000000..3a06970929f
--- /dev/null
+++ b/addons/point_of_sale/static/src/css/keyboard.css
@@ -0,0 +1,146 @@
+/* Onscreen Keyboard http://net.tutsplus.com/tutorials/javascript-ajax/creating-a-keyboard-with-css-and-jquery/ */
+
+.point-of-sale .keyboard_frame{
+ display: none;
+ position:absolute;
+ left: 0;
+ bottom: 0px;
+ margin: 0;
+ padding: 0;
+ padding-top: 15px;
+ width: 100%;
+ height: 0px; /* 235px, animated via jquery */
+ background-color: #BBB;
+ overflow:hidden;
+ -webkit-box-shadow: 0px 0px 10px rgba(0,0,0, 0.3);
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ z-index:10000;
+
+}
+.point-of-sale .keyboard_frame .close_button{
+ height:40px;
+ width:60px;
+ text-align:center;
+ background-color: #DDD;
+ font-size: 12px;
+ line-height:40px;
+ border: 1px solid #CCC;
+ -webkit-border-radius: 5px;
+ -webkit-box-shadow: 0px 2px 5px rgba(0,0,0, 0.2);
+ position:absolute;
+ top:0;
+ right:15px;
+ cursor: pointer;
+}
+.point-of-sale .keyboard li {
+ float: left;
+ text-align: center;
+ background-color: #fff;
+ border: 1px solid #f0f0f0;
+ top:0;
+ -moz-border-radius: 5px;
+ -webkit-border-radius: 5px;
+ -webkit-box-shadow: 0px 2px 5px rgba(0,0,0, 0.2);
+ -webkit-transition-property: top, background-color;
+ -webkit-transition-duration: 0.2s;
+ -webkit-transition-timing-function: linear;
+}
+.point-of-sale .keyboard li:hover {
+ position: relative;
+ top: 2px;
+ left: 0px;
+ border-color: #ddd;
+ background-color:#e5e5e5;
+ cursor: pointer;
+ -webkit-transition-property: top, background-color;
+ -webkit-transition-duration: 0.1s;
+ -webkit-transition-timing-function: ease-out;
+}
+.point-of-sale .uppercase {
+ text-transform: uppercase;
+}
+.point-of-sale .on {
+ display: none;
+}
+.point-of-sale .firstitem{
+ clear: left;
+}
+.point-of-sale .keyboard .lastitem {
+ margin-right: 0;
+}
+
+/* ---- full sized keyboard ---- */
+
+.point-of-sale .full_keyboard {
+ list-style: none;
+ font-size: 14px;
+ width: 680px;
+ height: 100%;
+ margin-left: auto;
+ margin-right: auto;
+}
+.point-of-sale .full_keyboard li{
+ margin: 0 5px 5px 0;
+ width: 40px;
+ height: 40px;
+ line-height: 40px;
+}
+.point-of-sale .full_keyboard .tab, .point-of-sale .full_keyboard .delete {
+ width: 70px;
+}
+.point-of-sale .full_keyboard .capslock {
+ width: 80px;
+}
+.point-of-sale .full_keyboard .return {
+ width: 77px;
+}
+.point-of-sale .full_keyboard .left-shift {
+ width: 95px;
+}
+.point-of-sale .full_keyboard .right-shift {
+ width: 109px;
+}
+.point-of-sale .full_keyboard .space {
+ clear: left;
+ width: 673px;
+}
+
+/* ---- simplified keyboard ---- */
+
+.point-of-sale .simple_keyboard {
+ list-style: none;
+ font-size: 16px;
+ width: 545px;
+ height: 220px;
+ margin-left: auto;
+ margin-right: auto;
+}
+.point-of-sale .simple_keyboard li{
+ margin: 0 5px 5px 0;
+ width: 49px;
+ height: 49px;
+ line-height: 49px;
+}
+.point-of-sale .simple_keyboard .firstitem.row_asdf{
+ margin-left:25px;
+}
+.point-of-sale .simple_keyboard .firstitem.row_zxcv{
+ margin-left:55px;
+}
+.point-of-sale .simple_keyboard .delete{
+ width: 103px;
+}
+.point-of-sale .simple_keyboard .return{
+ width: 103px;
+}
+.point-of-sale .simple_keyboard .space{
+ width:268px;
+}
+.point-of-sale .simple_keyboard .numlock{
+ width:103px;
+}
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 09fbe3818f2..ba89b09eab5 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -1,3 +1,7 @@
+@font-face{
+ font-family: 'Inconsolata';
+ src: url(../fonts/Inconsolata.otf);
+}
.point-of-sale {
padding: 0;
@@ -11,17 +15,53 @@
top: 0;
width: 100%;
height: 100%;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
}
-.point-of-sale table {
- border-spacing: 0;
- border-collapse: collapse;
+
+.point-of-sale .shadow-top{
+ position: absolute;
+ top:0;
+ left:0;
+ right:0;
+ height:10px;
+ background: -webkit-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0));
+ background: -moz-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0));
+ background: -ms-linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0));
+ background: linear-gradient(top,rgba(0,0,0,0.09),rgba(0,0,0,0));
}
-.point-of-sale td {
- border: 1px solid #e9eaec;
+.point-of-sale .darker-shadow-top{
+ position: absolute;
+ top:0;
+ left:0;
+ right:0;
+ height:10px;
+ background: -webkit-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0));
+ background: -moz-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0));
+ background: -ms-linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0));
+ background: linear-gradient(top,rgba(0,0,0,0.15),rgba(0,0,0,0));
}
-.point-of-sale input {
- color: #555555;
+/* ********* The black loading screen ********* */
+
+.point-of-sale .loader{
+ background-color: #222;
+ position:absolute;
+ left:0px;
+ top:0px;
+ width:100%;
+ height:100%;
+ z-index: 999;
+ text-align: center;
}
+.point-of-sale .loader img{
+ position:absolute;
+ top:50%;
+ left:50%;
+}
+
+/* ********* Generic element styling ********* */
+
.point-of-sale a {
text-decoration: none;
color: #555555;
@@ -32,12 +72,12 @@
padding: 4px 10px;
font-size: 11px;
border: 1px solid #cacaca;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
border-radius: 4px;
background: #e2e2e2;
- background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
background: -webkit-gradient(linear, left top, left bottom, from(#f0f0f0), to(#e2e2e2));
+ background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
+ background: -ms-linear-gradient(#f0f0f0, #e2e2e2);
+ background: linear-gradient(#f0f0f0, #e2e2e2);
}
.point-of-sale ul, .point-of-sale ol {
padding: 0;
@@ -46,22 +86,26 @@
.point-of-sale li {
list-style-type: none;
}
-.point-of-sale button img {
- vertical-align: bottom;
-}
.point-of-sale .pos-right-align {
text-align: right;
}
.point-of-sale .pos-right-align input {
text-align: right;
+ border: 1px solid #cecbcb;
+ border-radius: 4px;
}
-.point-of-sale #container {
- width: 100%;
- height: 100%;
-}
+
+/* ********* The black header bar ********* */
+
+
.point-of-sale #topheader {
+ position:absolute;
+ left:0;
+ top:0;
width: 100%;
- height: 54px;
+ height: 33px;
+ margin:0;
+ padding:0;
color: gray;
border-top: solid 1px #d3d3d3;
border-bottom: solid 1px black;
@@ -69,57 +113,129 @@
background: -moz-linear-gradient(#7b7979, #393939);
background: -webkit-gradient(linear, left top, left bottom, from(#7b7979), to(#393939));
}
-.point-of-sale #topheader button {
- color: black;
- border: 1px solid black;
- background: #7f82ac;
- background: -moz-linear-gradient(#b2b3d7, #7f82ac);
- background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac));
-}
-.point-of-sale #branding, .point-of-sale #rightheader {
- float: left;
- overflow: hidden;
- height: 35px;
- padding: 10px;
-}
-.point-of-sale #rightheader {
- float: none;
- margin-left: 440px;
-}
-.point-of-sale #branding {
+
+/* a) The left part of the top-bar */
+
+.point-of-sale #branding{
+ position: absolute;
+ display: table-cell;
+ left:0;
+ top:0;
+ width:439px;
+ height:100%;
+ margin:0;
+ padding:0;
border-right: 1px solid #373737;
- text-align: left;
- width: 419px;
+ text-align:left;
+ line-height:100%;
+ vertical-align: middle;
}
.point-of-sale #branding img {
height: 32px;
width: 116px;
+ margin-left:5px;
+ vertical-align:middle;
}
-.point-of-sale #neworder-button {
+.point-of-sale #branding .username{
+ float:right;
+ color:#DDD;
+ font-size:16px;
+ margin-right:32px;
+ margin-top:10px;
+ font-style:italic;
+}
+
+/* b) The right part of the top-bar */
+
+.point-of-sale #rightheader {
+ position: absolute;
+ left:440px;
+ right:0;
+ top:0;
+ height:100%;
+}
+
+.point-of-sale #rightheader button {
+ color: black;
+ height:29px;
+ margin:2px;
+ margin-right:0px;
+ border: 1px solid black;
+ background: #7f82ac;
+ background: -webkit-gradient(linear, left top, left bottom, from(#b2b3d7), to(#7f82ac));
+ background: -moz-linear-gradient(#b2b3d7, #7f82ac);
+ background: -ms-linear-gradient(#b2b3d7, #7f82ac);
+ background: linear-gradient(#b2b3d7, #7f82ac);
+}
+
+.point-of-sale #rightheader button.neworder-button {
width: 32px;
- padding: 4px 10px;
+ margin-left:4px;
+ margin-right:4px;
}
-.point-of-sale #loggedas {
- float: right;
- padding: 5px 9px;
- text-align: center;
- color: white;
- border-left: 1px solid #373737;
+
+.point-of-sale div#order-selector {
+ display: inline;
}
-.point-of-sale #loggedas p {
- margin: 0 0 3px 0;
+.point-of-sale ol#orders {
+ display: inline;
}
+.point-of-sale li.order-selector-button {
+ display: inline;
+}
+.point-of-sale li.selected-order button {
+ font-weight: 900;
+}
+
+/* c) The notifications indicator */
+
+.point-of-sale .oe_pos_synch-notification{
+ float:right;
+ color: rgba(255,255,255,0.4);
+ padding: 8px;
+ line-height:16px;
+ font-size:16px;
+ vertical-align:middle;
+ font-style: italic;
+ cursor:pointer;
+}
+
+.point-of-sale .oe_pos_synch-notification .oe_status_red{
+ display:inline-block;
+ cursor:pointer;
+ width:16px; height:16px;
+ background: url("../img/gtk-no.png") no-repeat ;
+}
+
+.point-of-sale .oe_pos_synch-notification .oe_status_green{
+ display:inline-block;
+ width:16px; height:16px;
+ background: url("../img/gtk-yes.png") no-repeat;
+}
+
+/* ********* Contains everything below the bar ********* */
+
.point-of-sale #content {
width: 100%;
position: absolute;
- top: 56px;
+ top: 35px;
bottom: 0;
+ background: #F0EEEE;
}
+
+/* ********* The leftpane contains the order, numpad and paypad ********* */
+
.point-of-sale #leftpane {
- height: 100%;
- width: 440px;
- position: relative;
- border-right: solid 1px #afafb6;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ position:absolute;
+ left:0;
+ width:440px;
+ top:0px;
+ bottom:105px;
+ border-right: solid 1px #CECBCB;
background-color: white;
}
.point-of-sale #leftpane footer {
@@ -127,69 +243,12 @@
bottom: 0;
left: 0;
width: 100%;
- background-color: #e0e0e0;
- background-image: url(../img/headerbackground.jpg);
+ background: #F0EEEE;
white-space: nowrap;
}
-.point-of-sale #current-order {
- width: 100%;
- position: absolute;
- top: 0;
- bottom: 271px;
- overflow: auto;
-}
-.point-of-sale #current-order thead {
- background-color: #cccccc;
- background-image: url(../img/headerbackground.jpg);
- border: 0px;
- font-size: 12px;
- width: 440px;
-}
-.point-of-sale #current-order thead td {
- text-align: center;
- padding: 8px 0px;
- min-width: 40px;
- font-size: 12px;
-}
-.point-of-sale #current-order td {
- padding: 6px 4px;
- font-size: 11px;
- text-align: right;
- min-width: 40px;
- white-space: nowrap;
-}
-.point-of-sale #current-order td:first-child {
- width: 320px;
- padding: 6px;
- text-align: left;
- text-overflow: ellipsis;
-}
-.point-of-sale #current-order td:last-child {
- border-right: none;
-}
-.point-of-sale #current-order tr.selected {
- background-color: #e9eaf2;
-}
-.point-of-sale #current-order tr.selected td {
- border-top: 2px solid #d5d6e0;
- border-bottom: 1px solid #d5d6e0;
- padding-top: 5px;
- color: #555555;
-}
-.point-of-sale #amounts {
- background: white;
- border-bottom: solid 1px #d2d2d2;
- border-top: solid 1px #e9eaec;
- font-weight: bold;
- text-align: right;
- -webkit-margin-before: 0;
- -webkit-margin-after: 0;
-}
-.point-of-sale #amounts li {
- display: inline-block;
- padding: 8px;
- width: 29%;
-}
+
+/* ********* The paypad contains the payment buttons ********* */
+
.point-of-sale #paypad {
padding: 8px 4px 8px 8px;
display: inline-block;
@@ -204,13 +263,19 @@
vertical-align: middle;
color: #555555;
border-top: 1px solid #efefef;
+ font-size: 14px;
}
.point-of-sale #paypad button:hover {
color: white;
background: #7f82ac;
- background: -moz-linear-gradient(#9d9fc5, #7f82ac);
background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac));
+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
+ background: linear-gradient(#9d9fc5, #7f82ac);
}
+
+/* ********* The Numpad ********* */
+
.point-of-sale #numpad {
padding: 8px 8px 8px 4px;
display: inline-block;
@@ -228,17 +293,18 @@
.point-of-sale #numpad button:hover {
color: white;
background: #7f82ac;
- background: -moz-linear-gradient(#9d9fc5, #7f82ac);
background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac));
+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
+ background: linear-gradient(#9d9fc5, #7f82ac);
}
.point-of-sale #numpad .selected-mode {
color: white;
background: #7f82ac;
- background: -moz-linear-gradient(#9d9fc5, #7f82ac);
background: -webkit-gradient(linear, left top, left bottom, from(#9d9fc5), to(#7f82ac));
-}
-.point-of-sale .payment-button {
- font-size: 14px;
+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
+ background: linear-gradient(#9d9fc5, #7f82ac);
}
.point-of-sale .input-button {
font-size: 24px;
@@ -246,15 +312,18 @@
.point-of-sale .mode-button, .point-of-sale #numpad-delete, .point-of-sale #numpad-minus {
font-size: 14px;
}
+
+/* ********* The right pane contains the screens and headers ********* */
+
.point-of-sale #rightpane {
position: absolute;
top: 0;
- bottom: 0;
- left: 441px;
+ bottom: 105px;
+ left: 440px;
right: 0;
- height: 100%;
vertical-align: top;
}
+
.point-of-sale #rightpane header {
padding: 0;
height: 32px;
@@ -263,12 +332,33 @@
background: -moz-linear-gradient(white, #d3d3d3);
background: -webkit-gradient(linear, left top, left bottom, from(white), to(#d3d3d3));
}
+
+/* ********* The product list ********* */
+
.point-of-sale .product-list {
- overflow: auto;
- position: absolute;
- top: 72px;
- bottom: 0;
+ padding:10px;
}
+
+.point-of-sale .product-list-scroller{
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ width:100%;
+ height:100%;
+ overflow: hidden;
+}
+.point-of-sale .product-list-container {
+ position:absolute;
+ top:0px;
+ bottom:0px;
+ left:0px;
+ right:0px;
+ background: #eeedff;
+}
+
+/* a) the product list navigation bar */
+
.point-of-sale .breadcrumb li {
float: left;
line-height: 32px;
@@ -294,14 +384,15 @@
height: 19px;
margin: 6px 0;
}
+
+/* b) the search box */
+
.point-of-sale .searchbox {
position: absolute;
right: 2px;
}
.point-of-sale .searchbox input {
width: 130px;
- -moz-border-radius: 11px;
- -webkit-border-radius: 11px;
border-radius: 11px;
border: 1px solid #cecbcb;
padding: 3px 19px;
@@ -316,46 +407,106 @@
cursor: pointer;
display: none;
}
+
+/* c) the categories list */
+
.point-of-sale #categories {
+ /*background:#f0f0f0;*/
+ position: relative;
+ background-image: url('../img/bg.png');
border-bottom: 1px solid #cecbcb;
}
+.point-of-sale #categories .white-gradient{
+ position: absolute;
+ top:50%;
+ left:0;
+ right:0;
+ bottom:0;
+ background: -webkit-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0));
+ background: -moz-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0));
+ background: -ms-linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0));
+ background: linear-gradient(bottom,rgba(255,255,255,0.5),rgba(255,255,255,0));
+}
.point-of-sale #categories h4 {
display: inline-block;
margin: 9px 5px;
}
-.point-of-sale #categories ol {
- display: inline;
+
+.point-of-sale .category-list{
+ padding:10px;
}
-.point-of-sale #categories li {
- display: inline-block;
-}
-.point-of-sale #categories .button {
- padding: 6px 14px;
- margin: 4px 0;
- font-size: 12px;
-}
-.point-of-sale .product {
+/* d) the category button */
+
+.point-of-sale .category-button {
+ position: relative;
vertical-align: top;
display: inline-block;
font-size: 11px;
margin: 5px;
- max-width: 120px;
- border: 1px solid lightgray;
- -moz-border-radius: 4px;
- -webkit-border-radius: 4px;
- border-radius: 4px;
- -moz-box-shadow: 0px 1px 4px #777777;
- -webkit-box-shadow: 0px 1px 4px #777777;
- -box-shadow: 0px 1px 4px #777777;
+ width: 120px;
+ height:120px;
+ background:#fff;
+ border: 1px solid #fff;
+ border-radius: 3px;
+ -webkit-box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ -moz-box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ cursor: pointer;
}
-.point-of-sale .product-img {
+
+.point-of-sale .category-button .category-img {
+ position: relative;
+ width: 120px;
+ height: 100px;
+ text-align: center;
+ cursor: pointer;
+}
+
+.point-of-sale .category-button .category-name {
+ position: absolute;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ top:auto;
+ left: 2px;
+ right: 2px;
+ bottom: 2px;
+ background: #8a89ba; /*-webkit-linear-gradient(-90deg,rgba(138,137,186,0),rgba(138,137,186,1), rgba(138,137,186,1));*/
+ padding: 3px;
+ /*color:#8a89ba;*/
+ color: #FFF;
+ cursor: pointer;
+ border-radius: 3px;
+}
+
+/* e) the product */
+
+.point-of-sale .product {
+ position:relative;
+ vertical-align: top;
+ display: inline-block;
+ font-size: 11px;
+ margin: 5px;
+ width: 120px;
+ height:120px;
+ background:#fff;
+ border: 1px solid #fff;
+ border-radius: 3px;
+ -webkit-box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ -moz-box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+}
+
+.point-of-sale .product .product-img {
position: relative;
width: 120px;
height: 100px;
background: white;
text-align: center;
}
-.point-of-sale .price-tag {
+
+.point-of-sale .product .price-tag {
position: absolute;
top: 2px;
right: 2px;
@@ -363,41 +514,77 @@
color: white;
background: #7f82ac;
padding: 2px 5px;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
border-radius: 3px;
}
-.point-of-sale .product-name {
+
+.point-of-sale .product .product-name {
+ position: absolute;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ bottom:0;
+ top:auto;
+ width:100%;
+ background: -webkit-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
+ background: -moz-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
+ background: -ms-linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
+ background: linear-gradient(-90deg,rgba(255,255,255,0),rgba(255,255,255,1), rgba(255,255,255,1));
+ /*background:#FFF;*/
padding: 3px;
-}
-.point-of-sale #login-form label, .point-of-sale #login-form input {
- display: block;
-}
-.point-of-sale #login-form input {
- margin: 4px 0 12px;
- padding: 4px;
- width: 96%;
-}
-.point-of-sale div#order-selector {
- display: inline;
-}
-.point-of-sale ol#orders {
- display: inline;
-}
-.point-of-sale li.order-selector-button {
- display: inline;
-}
-.point-of-sale li.selected-order button {
- font-weight: 900;
+ padding-top:15px;
}
-.point-of-sale .step-screen {
+
+/* ********* The Screens ********* */
+
+.point-of-sale .screen {
+ position:absolute;
text-align: center;
+ top:0px;
+ bottom:0px;
+ width:100%;
}
-.point-of-sale .step-screen header h2 {
+.point-of-sale .screen header h2 {
margin-top: 0px;
padding-top: 7px;
}
+.point-of-sale .screen p{
+ font-size: 18px;
+}
+
+/* a) Layout for the Product Screen */
+
+.point-of-sale .screen .layout-table {
+ border:none;
+ width:100%;
+ height:100%;
+}
+
+.point-of-sale .screen .header-row {
+ border:none;
+ width:100%;
+ height:0px;
+}
+
+.point-of-sale .screen .header-cell{
+ border:none;
+ width:100%;
+ height:0px;
+}
+.point-of-sale .screen .content-row {
+ width:100%;
+ height:100%;
+}
+.point-of-sale .screen .content-cell{
+ width:100%;
+}
+.point-of-sale .screen .content-cell .content-container{
+ height:100%;
+ position:relative;
+}
+
+/* b) The payment screen */
.point-of-sale .pos-step-container {
display: inline-block;
@@ -406,30 +593,48 @@
.point-of-sale .pos-payment-container {
text-align: left;
}
-.point-of-sale .pos-payment-container .payment-due {
- display: block;
- margin-top: 10px;
- margin-bottom: 10px;
- padding: 3px 6px 0px 6px;
- background-color: white;
- border:1px solid grey;
- border-radius: 3px;
+.point-of-sale .pos-payment-container .left-block{
+ display: inline-block;
+ width:49%;
+ margin:0;
+ padding:0;
+ text-align:left;
+}
+.point-of-sale .pos-payment-container .header{
+ margin-top: 50px;
+ margin-bottom:20px;
+ font-weight: bold;
+}
+.point-of-sale .pos-payment-container .infoline{
+ margin-top:5px;
+ margin-bottom:5px;
+}
+.point-of-sale .pos-payment-container .right-block{
+ display: inline-block;
+ width:49%;
+ margin:0;
+ padding:0;
+ text-align:right;
}
.point-of-sale .pos-payment-container table {
width: 100%;
margin-bottom: 20px;
}
+.point-of-sale .pos-payment-container td {
+ vertical-align: middle;
+}
.point-of-sale .pos-payment-container .paymentline-type {
- font-size: 0.8em;
+ font-size: 1em;
font-weight: bold;
+ margin-right:10px;
}
-.point-of-sale .step-screen button {
- width: 50%;
- text-align: center;
- padding: 7px 0 7px 0;
- font-size: 0.8em;
- font-weight: bold;
+
+/* c) The receipt screen */
+
+.point-of-sale .pos-receipt-container {
+ font-size: 0.75em;
}
+
.point-of-sale .pos-sale-ticket {
text-align: left;
width: 300px;
@@ -445,25 +650,6 @@
.point-of-sale .pos-sale-ticket table td {
border: 0;
}
-.point-of-sale .pos-receipt-container {
- font-size: 0.75em;
-}
-
-.point-of-sale .oe_pos_synch-notification-button {
- color: white;
- border: 1px solid black;
- border-radius: 3px;
- padding: 2px 3px 2px 3px;
- background-color: #D92A2A;
-}
-
-.receipt-buttons {
- white-space: nowrap;
-}
-
-.pos-payment-buttons {
- white-space: nowrap;
-}
@media print {
#oe_header, #oe_menu, .point-of-sale #topheader, .point-of-sale #leftpane {
@@ -476,7 +662,7 @@
left: 0px;
background-color: white;
}
- #receipt-screen header, .receipt-buttons {
+ #receipt-screen header {
display: none;
}
#receipt-screen {
@@ -487,3 +673,444 @@
}
}
+/* d) The Scale screen */
+
+.point-of-sale .scale-screen .display{
+ position:relative;
+ width:600px;
+ height:190px;
+ margin-top: 100px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.point-of-sale .scale-screen .product-picture {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ display: float;
+ float: right;
+ margin: 5px;
+ width: 180px;
+ height:180px;
+ line-height:180px;
+ cursor:pointer;
+
+ background:#fff;
+ border: 1px solid #fff;
+ border-radius: 3px;
+ -webkit-box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ -moz-box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+ box-shadow: 0px 1px 8px rgba(0,0,0,0.2);
+}
+
+.point-of-sale .scale-screen .product-picture img{
+ vertical-align: middle;
+ cursor:pointer;
+}
+
+.point-of-sale .scale-screen .product-picture .product-price{
+ position: absolute;
+ top:8px;
+ right:8px;
+ width:auto;
+ height:auto;
+ line-height:1;
+ color:white;
+ background: #7f82ac;
+ padding: 2px 5px;
+ border-radius: 3px;
+ cursor:pointer;
+}
+
+.point-of-sale .scale-screen .product-name {
+ position: absolute;
+ left:0;
+ top:20px;
+ height:50px;
+ font-size:40px;
+ line-height:50px;
+ text-align:right;
+ right:225px;
+}
+.point-of-sale .scale-screen .weight{
+ position: absolute;
+ left:0;
+ height:90px;
+ bottom:15px;
+ right:220px;
+ padding:5px;
+}
+.point-of-sale .scale-screen .weight p{
+ display: inline-block;
+ text-align:right;
+ line-height: 90px;
+ font-size: 80px;
+ width:100%;
+ height:100%;
+ margin:0;
+ font-family: "Inconsolata";
+}
+
+/* ********* The OrderWidget ********* */
+
+.point-of-sale .order-container{
+ position: absolute;
+ top: 0px;
+ bottom: 232px;
+ width:100%;
+ background: #F0EEEE;
+}
+
+.point-of-sale .order-scroller{
+ width:100%;
+ height:100%;
+ overflow:hidden;
+}
+
+.point-of-sale .order{
+ background: #F00;
+ background: -webkit-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1));
+ background: -moz-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1));
+ background: -ms-linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1));
+ background: linear-gradient(0deg,rgba(245,245,245,1),rgba(255,255,255,1), rgba(245,245,245,1));
+ padding-bottom:15px;
+ padding-top:15px;
+ margin-left:16px;
+ margin-right:16px;
+ margin-top:16px;
+ margin-bottom:16px;
+ font-size:16px;
+ -webkit-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3);
+ -moz-box-shadow: 0px 5px 16px rgba(0,0,0, 0.3);
+ box-shadow: 0px 5px 16px rgba(0,0,0, 0.3);
+
+}
+
+.point-of-sale .order .empty{
+ text-align:center;
+ margin-top: 15px;
+ margin-bottom: 5px;
+ color:#999;
+ font-weight: normal;
+}
+
+.point-of-sale .order .summary{
+ width:100%;
+ text-align:right;
+ font-weight: bold;
+ margin-top:20px;
+ margin-bottom:10px;
+}
+.point-of-sale .order .summary .line{
+ margin-right:15px;
+ padding-top:5px;
+ border-top: solid 2px;
+ border-color:#777;
+}
+.point-of-sale .order .summary .line.empty{
+ border-color:#BBB;
+ color:#999;
+}
+
+/* ********* The OrderLineWidget ********* */
+
+.point-of-sale .order .orderline{
+ width:100%;
+ margin:0px;
+ padding-top:3px;
+ padding-bottom:10px;
+ padding-left:15px;
+ padding-right:15px;
+ cursor: pointer;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-transition: background 250ms ease-in-out;
+ -moz-transition: background 250ms ease-in-out;
+ transition: background 250ms ease-in-out;
+}
+.point-of-sale .order .orderline:hover{
+ background: rgba(140,143,183,0.05);
+ -webkit-transition: background 50ms ease-in-out;
+ -moz-transition: background 50ms ease-in-out;
+ transition: background 50ms ease-in-out;
+}
+
+.point-of-sale .order .orderline.selected{
+ background: rgba(140,143,183,0.2);
+ -webkit-transition: background 250ms ease-in-out;
+ -moz-transition: background 250ms ease-in-out;
+ transition: background 250ms ease-in-out;
+ cursor: default;
+}
+.point-of-sale .order .orderline .product-name{
+ padding:0;
+ display:inline-block;
+ font-weight: bold;
+ width:80%;
+ overflow:hidden;
+}
+.point-of-sale .order .orderline .price{
+ padding:0;
+ font-weight: bold;
+ float:right;
+}
+.point-of-sale .order .orderline .info-list{
+ color: #888;
+ margin-left:10px;
+}
+.point-of-sale .order .orderline .info-list em{
+ color: #777;
+ font-weight: bold;
+ font-style:normal;
+}
+
+/* ********* The ActionBarWidget ********* */
+
+.point-of-sale .pos-actionbar{
+ position:absolute;
+ left: 0;
+ bottom: 0px;
+ height: 105px;
+ width: 100%;
+ margin: 0;
+ background: #f5f5f5; /*#ebebeb;*/
+ border-top: solid 1px #afafb6;
+ z-index:900;
+}
+
+.point-of-sale .pos-actionbar ul{
+ list-style: none;
+}
+
+.point-of-sale .pos-actionbar-left-pane{
+ height: 100%;
+ width: 434px;
+ margin: 0px;
+ padding-left:3px;
+ padding-right:3px;
+ border-right: solid 1px #dfdfdf;
+ float: left;
+}
+
+.point-of-sale .pos-actionbar-button-list{
+ height: 100%;
+ margin: 0px;
+ padding-left:3px;
+ padding-right:3px;
+ overflow:hidden;
+}
+
+.point-of-sale .pos-actionbar .button{
+ width: 90px;
+ height: 90px;
+ text-align:center;
+ margin:3px;
+ margin-top:6px;
+ float:left;
+
+ font-size: 14px;
+ font-weight: bold;
+
+ cursor: pointer;
+
+ border: 1px solid #cacaca;
+ border-radius: 4px;
+
+ background: #e2e2e2;
+ background: -webkit-linear-gradient(#f0f0f0, #e2e2e2);
+ background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
+ background: -ms-linear-gradient(#f0f0f0, #e2e2e2);
+ background: linear-gradient(#f0f0f0, #e2e2e2);
+ -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
+ -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
+ box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
+}
+.point-of-sale .pos-actionbar .button .label{
+ margin-top: 37px;
+}
+.point-of-sale .pos-actionbar .button .icon{
+ margin-top: 10px;
+}
+.point-of-sale .pos-actionbar .button:hover {
+ color: white;
+ background: #7f82ac;
+ border: 1px solid #7f82ac;
+ background: -webkit-linear-gradient(#9d9fc5, #7f82ac);
+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
+ background: linear-gradient(#9d9fc5, #7f82ac);
+
+ -webkit-transition-property: background, border;
+ -webkit-transition-duration: 0.2s;
+ -webkit-transition-timing-function: ease-out;
+}
+
+.point-of-sale .pos-actionbar .button.rightalign{
+ float:right;
+}
+
+/* ********* The PopupWidgets ********* */
+
+.point-of-sale .modal-dialog{
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height:100%;
+ background-color: rgba(0,0,0,0.5);
+ z-index:1000;
+}
+.point-of-sale .modal-dialog .popup{
+ position: absolute;
+ left:50%;
+ top:50%;
+ width:500px;
+ height:400px;
+ margin-left: -250px;
+ margin-top: -200px;
+ padding:10px;
+ padding-top:20px;
+ text-align:center;
+ font-size:20px;
+ font-weight:bold;
+ background-color: #F0EEEE;
+ border: 1px solid #E0DDDD;
+ -webkit-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3);
+ -moz-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3);
+ -ms-box-shadow: 0px 10px 20px rgba(0,0,0, 0.3);
+ z-index:1200;
+}
+.point-of-sale .popup .footer{
+ position:absolute;
+ bottom:0;
+ left:0;
+ width:100%;
+ height:60px;
+ border-top: 1px solid #E0DDDD;
+}
+.point-of-sale .popup .button{
+ float:right;
+ width: 110px;
+ height: 40px;
+ line-height:40px;
+ text-align:center;
+ margin:3px;
+ margin-top:10px;
+ margin-right:10px;
+
+ font-size: 14px;
+ font-weight: bold;
+
+ cursor: pointer;
+
+ border: 1px solid #cacaca;
+ border-radius: 4px;
+
+ background: #e2e2e2;
+ background: -webkit-linear-gradient(#f0f0f0, #e2e2e2);
+ background: -moz-linear-gradient(#f0f0f0, #e2e2e2);
+ background: -ms-linear-gradient(#f0f0f0, #e2e2e2);
+ background: linear-gradient(#f0f0f0, #e2e2e2);
+ -webkit-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
+ -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
+ box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
+}
+.point-of-sale .popup .button:hover {
+ color: white;
+ background: #7f82ac;
+ border: 1px solid #7f82ac;
+ background: -webkit-linear-gradient(#9d9fc5, #7f82ac);
+ background: -moz-linear-gradient(#9d9fc5, #7f82ac);
+ background: -ms-linear-gradient(#9d9fc5, #7f82ac);
+ background: linear-gradient(#9d9fc5, #7f82ac);
+
+ -webkit-transition-property: background, border;
+ -webkit-transition-duration: 0.2s;
+ -webkit-transition-timing-function: ease-out;
+}
+
+.point-of-sale .popup .button.big-left{
+ position:absolute;
+ top: 120px;
+ left:40px;
+ width: 180px;
+ height: 180px;
+ line-height:180px;
+}
+
+.point-of-sale .popup .button.big-right{
+ position:absolute;
+ top: 120px;
+ right:40px;
+ width: 180px;
+ height: 180px;
+ line-height:180px;
+}
+
+/* ********* The ScrollBarWidget ********* */
+
+.point-of-sale .scrollbar{
+ position:absolute;
+ top:7px;
+ right:7px;
+ width:48px;
+ bottom:7px;
+ background: rgba(0,0,0,0.1);
+
+}
+.point-of-sale .scrollbar .button{
+ width:100%;
+ height: 48px;
+ line-height: 38px;
+ text-align: center;
+ font-size:48px;
+ border-radius: 4px;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all 250ms ease-in-out;
+}
+.point-of-sale .scrollbar .button{
+ color:white;
+ background: rgba(0,0,0,0.6);
+ -webkit-box-shadow: 0px 1px 4px rgba(0,0,0,0.01);
+ -moz-box-shadow: 0px 1px 4px rgba(0,0,0,0.01);
+ box-shadow: 0px 1px 4px rgba(0,0,0,0.01);
+ text-shadow: rgba(255,255,255,0.5) 0px 0px 10px;
+ -webkit-transition: all 250ms ease-in-out;
+ -moz-transition: all 250ms ease-in-out;
+ transition: all 250ms ease-in-out;
+}
+.point-of-sale .scrollbar .button:hover{
+ text-shadow: rgba(255,255,255,0.8) 0px 0px 15px;
+}
+.point-of-sale .scrollbar .button.disabled{
+ background: rgba(0,0,0,0.3);
+ color:rgba(255,255,255,0.5);
+ -webkit-transition: all 250ms ease-in-out;
+ -moz-transition: all 250ms ease-in-out;
+ transition: all 250ms ease-in-out;
+}
+.point-of-sale .scrollbar .down-button{
+ position:absolute;
+ bottom:0px;
+}
+.point-of-sale .scrollbar .up-button{
+ position:absolute;
+ top:0px;
+}
+.point-of-sale .scrollbar .scroller{
+ position:absolute;
+ top:33%;
+ bottom:50%;
+ width:100%;
+ background: rgba(0,0,0,0.1);
+ border-radius: 4px;
+}
+
diff --git a/addons/point_of_sale/static/src/fonts/Inconsolata.otf b/addons/point_of_sale/static/src/fonts/Inconsolata.otf
new file mode 100644
index 00000000000..348889828d8
Binary files /dev/null and b/addons/point_of_sale/static/src/fonts/Inconsolata.otf differ
diff --git a/addons/point_of_sale/static/src/img/bancontact.png b/addons/point_of_sale/static/src/img/bancontact.png
new file mode 100644
index 00000000000..5c34fefb0f9
Binary files /dev/null and b/addons/point_of_sale/static/src/img/bancontact.png differ
diff --git a/addons/point_of_sale/static/src/img/bg.png b/addons/point_of_sale/static/src/img/bg.png
new file mode 100644
index 00000000000..d54b6302d5a
Binary files /dev/null and b/addons/point_of_sale/static/src/img/bg.png differ
diff --git a/addons/point_of_sale/static/src/img/gtk-no.png b/addons/point_of_sale/static/src/img/gtk-no.png
new file mode 100644
index 00000000000..047ddcd9289
Binary files /dev/null and b/addons/point_of_sale/static/src/img/gtk-no.png differ
diff --git a/addons/point_of_sale/static/src/img/gtk-yes.png b/addons/point_of_sale/static/src/img/gtk-yes.png
new file mode 100644
index 00000000000..01fb373c251
Binary files /dev/null and b/addons/point_of_sale/static/src/img/gtk-yes.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/face-monkey.png b/addons/point_of_sale/static/src/img/icons/png48/face-monkey.png
new file mode 100644
index 00000000000..1a57c9e0696
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/face-monkey.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/go-next.png b/addons/point_of_sale/static/src/img/icons/png48/go-next.png
new file mode 100644
index 00000000000..bcd343dde94
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/go-next.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/go-previous.png b/addons/point_of_sale/static/src/img/icons/png48/go-previous.png
new file mode 100644
index 00000000000..0a084ad66eb
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/go-previous.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/help.png b/addons/point_of_sale/static/src/img/icons/png48/help.png
new file mode 100644
index 00000000000..8c56bd4f962
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/help.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/printer.png b/addons/point_of_sale/static/src/img/icons/png48/printer.png
new file mode 100644
index 00000000000..9747f37ddcc
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/printer.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/scale.png b/addons/point_of_sale/static/src/img/icons/png48/scale.png
new file mode 100644
index 00000000000..3070c56ce1c
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/scale.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/shut-down.png b/addons/point_of_sale/static/src/img/icons/png48/shut-down.png
new file mode 100644
index 00000000000..8478ab19686
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/shut-down.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/system-log-out.png b/addons/point_of_sale/static/src/img/icons/png48/system-log-out.png
new file mode 100644
index 00000000000..edd6e3446a0
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/system-log-out.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/png48/validate.png b/addons/point_of_sale/static/src/img/icons/png48/validate.png
new file mode 100644
index 00000000000..75372c89533
Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/validate.png differ
diff --git a/addons/point_of_sale/static/src/img/icons/printer.svg b/addons/point_of_sale/static/src/img/icons/printer.svg
new file mode 100644
index 00000000000..4c702dc48e2
--- /dev/null
+++ b/addons/point_of_sale/static/src/img/icons/printer.svg
@@ -0,0 +1,502 @@
+
+
+
diff --git a/addons/point_of_sale/static/src/img/icons/scale.svg b/addons/point_of_sale/static/src/img/icons/scale.svg
new file mode 100644
index 00000000000..a04a62adeba
--- /dev/null
+++ b/addons/point_of_sale/static/src/img/icons/scale.svg
@@ -0,0 +1,797 @@
+
+
\ No newline at end of file
diff --git a/addons/point_of_sale/static/src/img/icons/validate.svg b/addons/point_of_sale/static/src/img/icons/validate.svg
new file mode 100644
index 00000000000..be4c67536ad
--- /dev/null
+++ b/addons/point_of_sale/static/src/img/icons/validate.svg
@@ -0,0 +1,201 @@
+
+
+
+
diff --git a/addons/point_of_sale/static/src/img/loader.gif b/addons/point_of_sale/static/src/img/loader.gif
new file mode 100644
index 00000000000..967288f55af
Binary files /dev/null and b/addons/point_of_sale/static/src/img/loader.gif differ
diff --git a/addons/point_of_sale/static/src/img/scale.png b/addons/point_of_sale/static/src/img/scale.png
new file mode 100644
index 00000000000..8f66d1f9ead
Binary files /dev/null and b/addons/point_of_sale/static/src/img/scale.png differ
diff --git a/addons/point_of_sale/static/src/img/scan.png b/addons/point_of_sale/static/src/img/scan.png
new file mode 100644
index 00000000000..e5eafbe821d
Binary files /dev/null and b/addons/point_of_sale/static/src/img/scan.png differ
diff --git a/addons/point_of_sale/static/src/js/pos.js b/addons/point_of_sale/static/src/js/pos.js
deleted file mode 100644
index d62e56cab30..00000000000
--- a/addons/point_of_sale/static/src/js/pos.js
+++ /dev/null
@@ -1,1415 +0,0 @@
-openerp.point_of_sale = function(db) {
-
- db.point_of_sale = {};
-
- var __extends = function(child, parent) {
- var __hasProp = Object.prototype.hasOwnProperty;
- for (var key in parent) {
- if (__hasProp.call(parent, key))
- child[key] = parent[key];
- }
- function ctor() {
- this.constructor = child;
- }
-
- ctor.prototype = parent.prototype;
- child.prototype = new ctor;
- child.__super__ = parent.prototype;
- return child;
- };
-
- var QWeb = db.web.qweb;
- var qweb_template = function(template) {
- return function(ctx) {
- return QWeb.render(template, _.extend({}, ctx,{
- 'currency': pos.get('currency'),
- 'format_amount': function(amount) {
- if (pos.get('currency').position == 'after') {
- return amount + ' ' + pos.get('currency').symbol;
- } else {
- return pos.get('currency').symbol + ' ' + amount;
- }
- },
- }));
- };
- };
- var _t = db.web._t;
-
- var DAOInterface = {
- add_operation: function(operation) {},
- remove_operation: function(id) {},
- get_operations: function() {},
- };
- var LocalStorageDAO = db.web.Class.extend({
- add_operation: function(operation) {
- var self = this;
- return $.async_when().pipe(function() {
- var tmp = self._get('oe_pos_operations', []);
- var last_id = self._get('oe_pos_operations_sequence', 1);
- tmp.push({'id': last_id, 'data': operation});
- self._set('oe_pos_operations', tmp);
- self._set('oe_pos_operations_sequence', last_id + 1);
- });
- },
- remove_operation: function(id) {
- var self = this;
- return $.async_when().pipe(function() {
- var tmp = self._get('oe_pos_operations', []);
- tmp = _.filter(tmp, function(el) {
- return el.id !== id;
- });
- self._set('oe_pos_operations', tmp);
- });
- },
- get_operations: function() {
- var self = this;
- return $.async_when().pipe(function() {
- return self._get('oe_pos_operations', []);
- });
- },
- _get: function(key, default_) {
- var txt = localStorage[key];
- if (! txt)
- return default_;
- return JSON.parse(txt);
- },
- _set: function(key, value) {
- localStorage[key] = JSON.stringify(value);
- },
- });
-
- var fetch = function(osvModel, fields, domain) {
- var dataSetSearch;
- dataSetSearch = new db.web.DataSetSearch(null, osvModel, {}, domain);
- return dataSetSearch.read_slice(fields, 0);
- };
-
- /*
- Gets all the necessary data from the OpenERP web client (session, shop data etc.)
- */
- var Pos = Backbone.Model.extend({
- initialize: function(session, attributes) {
- Backbone.Model.prototype.initialize.call(this, attributes);
- this.dao = new LocalStorageDAO();
- this.ready = $.Deferred();
- this.flush_mutex = new $.Mutex();
- this.build_tree = _.bind(this.build_tree, this);
- this.session = session;
- this.set({'nbr_pending_operations': 0,
- 'currency': {symbol: '$', position: 'after'},
- 'shop': {},
- 'company': {},
- 'user': {}});
-
- var self = this;
- var cat_def = fetch('pos.category', ['name', 'parent_id', 'child_id']).pipe(function(result) {
- return self.set({'categories': result});
- });
- var prod_def = fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id',
- 'product_image_small'], [['pos_categ_id', '!=', 'false']]).then(function(result) {
- return self.set({'product_list': result});
- });
- var bank_def = fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name'],
- [['state', '=', 'open'], ['user_id', '=', this.session.uid]]).then(function(result) {
- return self.set({'bank_statements': result});
- });
- var tax_def = fetch('account.tax', ['amount', 'price_include', 'type']).then(function(result) {
- return self.set({'taxes': result});
- });
- $.when(cat_def, prod_def, bank_def, tax_def, this.get_app_data(), this.flush())
- .pipe(_.bind(this.build_tree, this));
- },
- get_app_data: function() {
- var self = this;
- return $.when(new db.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
- self.set({'shop': result[0]});
- var company_id = result[0]['company_id'][0];
- return new db.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
- self.set({'company': result});
- var currency_id = result['currency_id'][0]
- return new db.web.Model("res.currency").get_func("read")([currency_id],
- ['symbol', 'position']).pipe(function(result) {
- self.set({'currency': result[0]});
-
- });
- });
- }), new db.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
- self.set({'user': result});
- }));
- },
- pushOrder: function(record) {
- var self = this;
- return this.dao.add_operation(record).pipe(function() {
- return self.flush();
- });
- },
- flush: function() {
- return this.flush_mutex.exec(_.bind(function() {
- return this._int_flush();
- }, this));
- },
- _int_flush : function() {
- var self = this;
- this.dao.get_operations().pipe(function(ops) {
- self.set({"nbr_pending_operations": ops.length});
- if (ops.length === 0)
- return $.when();
- var op = ops[0].data;
- var op_id = ops[0].id;
- /* we prevent the default error handler and assume errors
- * are a normal use case, except we stop the current iteration
- */
- return new db.web.Model("pos.order").get_func("create_from_ui")([op]).fail(function(unused, event) {
- event.preventDefault();
- }).pipe(function() {
- console.debug('saved 1 record');
- self.dao.remove_operation(op_id).pipe(function() {
- return self._int_flush();
- });
- }, function() {
- return $.when();
- });
- });
- },
- categories: {},
- build_tree: function() {
- var c, id, _i, _len, _ref, _ref2;
- _ref = this.get('categories');
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- c = _ref[_i];
- this.categories[c.id] = {
- id: c.id,
- name: c.name,
- children: c.child_id,
- parent: c.parent_id[0],
- ancestors: [c.id],
- subtree: [c.id]
- };
- }
- _ref2 = this.categories;
- for (id in _ref2) {
- c = _ref2[id];
- this.current_category = c;
- this.build_ancestors(c.parent);
- this.build_subtree(c);
- }
- this.categories[0] = {
- ancestors: [],
- children: (function() {
- var _j, _len2, _ref3, _results;
- _ref3 = this.get('categories');
- _results = [];
- for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
- c = _ref3[_j];
- if (!(c.parent_id[0] != null)) {
- _results.push(c.id);
- }
- }
- return _results;
- }).call(this),
- subtree: (function() {
- var _j, _len2, _ref3, _results;
- _ref3 = this.get('categories');
- _results = [];
- for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
- c = _ref3[_j];
- _results.push(c.id);
- }
- return _results;
- }).call(this)
- };
- return this.ready.resolve();
- },
- build_ancestors: function(parent) {
- if (parent != null) {
- this.current_category.ancestors.unshift(parent);
- return this.build_ancestors(this.categories[parent].parent);
- }
- },
- build_subtree: function(category) {
- var c, _i, _len, _ref, _results;
- _ref = category.children;
- _results = [];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- c = _ref[_i];
- this.current_category.subtree.push(c);
- _results.push(this.build_subtree(this.categories[c]));
- }
- return _results;
- }
- });
-
- /* global variable */
- var pos;
-
- /*
- ---
- Models
- ---
- */
- var CashRegister = (function() {
- __extends(CashRegister, Backbone.Model);
- function CashRegister() {
- CashRegister.__super__.constructor.apply(this, arguments);
- }
-
- return CashRegister;
- })();
- var CashRegisterCollection = (function() {
- __extends(CashRegisterCollection, Backbone.Collection);
- function CashRegisterCollection() {
- CashRegisterCollection.__super__.constructor.apply(this, arguments);
- }
-
- CashRegisterCollection.prototype.model = CashRegister;
- return CashRegisterCollection;
- })();
- var Product = (function() {
- __extends(Product, Backbone.Model);
- function Product() {
- Product.__super__.constructor.apply(this, arguments);
- }
-
- return Product;
- })();
- var ProductCollection = (function() {
- __extends(ProductCollection, Backbone.Collection);
- function ProductCollection() {
- ProductCollection.__super__.constructor.apply(this, arguments);
- }
-
- ProductCollection.prototype.model = Product;
- return ProductCollection;
- })();
- var Category = (function() {
- __extends(Category, Backbone.Model);
- function Category() {
- Category.__super__.constructor.apply(this, arguments);
- }
-
- return Category;
- })();
- var CategoryCollection = (function() {
- __extends(CategoryCollection, Backbone.Collection);
- function CategoryCollection() {
- CategoryCollection.__super__.constructor.apply(this, arguments);
- }
-
- CategoryCollection.prototype.model = Category;
- return CategoryCollection;
- })();
- /*
- Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
- There should only ever be one Orderline per distinct product in an Order.
- To add more of the same product, just update the quantity accordingly.
- The Order also contains payment information.
- */
- var Orderline = Backbone.Model.extend({
- defaults: {
- quantity: 1,
- list_price: 0,
- discount: 0
- },
- initialize: function(attributes) {
- Backbone.Model.prototype.initialize.apply(this, arguments);
- this.bind('change:quantity', function(unused, qty) {
- if (qty == 0)
- this.trigger('killme');
- }, this);
- },
- incrementQuantity: function() {
- return this.set({
- quantity: (this.get('quantity')) + 1
- });
- },
- getPriceWithoutTax: function() {
- return this.getAllPrices().priceWithoutTax;
- },
- getPriceWithTax: function() {
- return this.getAllPrices().priceWithTax;
- },
- getTax: function() {
- return this.getAllPrices().tax;
- },
- getAllPrices: function() {
- var self = this;
- var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
- var totalTax = base;
- var totalNoTax = base;
-
- var product_list = pos.get('product_list');
- var product = _.detect(product_list, function(el) {return el.id === self.get('id');});
- var taxes_ids = product.taxes_id;
- var taxes = pos.get('taxes');
- var taxtotal = 0;
- _.each(taxes_ids, function(el) {
- var tax = _.detect(taxes, function(t) {return t.id === el;});
- if (tax.price_include) {
- var tmp;
- if (tax.type === "percent") {
- tmp = base - (base / (1 + tax.amount));
- } else if (tax.type === "fixed") {
- tmp = tax.amount * self.get('quantity');
- } else {
- throw "This type of tax is not supported by the point of sale: " + tax.type;
- }
- taxtotal += tmp;
- totalNoTax -= tmp;
- } else {
- var tmp;
- if (tax.type === "percent") {
- tmp = tax.amount * base;
- } else if (tax.type === "fixed") {
- tmp = tax.amount * self.get('quantity');
- } else {
- throw "This type of tax is not supported by the point of sale: " + tax.type;
- }
- taxtotal += tmp;
- totalTax += tmp;
- }
- });
- return {
- "priceWithTax": totalTax,
- "priceWithoutTax": totalNoTax,
- "tax": taxtotal,
- };
- },
- exportAsJSON: function() {
- var result;
- result = {
- qty: this.get('quantity'),
- price_unit: this.get('list_price'),
- discount: this.get('discount'),
- product_id: this.get('id')
- };
- return result;
- },
- });
- var OrderlineCollection = Backbone.Collection.extend({
- model: Orderline,
- });
- /*
- Every PaymentLine has all the attributes of the corresponding CashRegister.
- */
- var Paymentline = (function() {
- __extends(Paymentline, Backbone.Model);
- function Paymentline() {
- Paymentline.__super__.constructor.apply(this, arguments);
- }
-
- Paymentline.prototype.defaults = {
- amount: 0
- };
- Paymentline.prototype.getAmount = function() {
- return this.get('amount');
- };
- Paymentline.prototype.exportAsJSON = function() {
- var result;
- result = {
- name: db.web.datetime_to_str(new Date()),
- statement_id: this.get('id'),
- account_id: (this.get('account_id'))[0],
- journal_id: (this.get('journal_id'))[0],
- amount: this.getAmount()
- };
- return result;
- };
- return Paymentline;
- })();
- var PaymentlineCollection = (function() {
- __extends(PaymentlineCollection, Backbone.Collection);
- function PaymentlineCollection() {
- PaymentlineCollection.__super__.constructor.apply(this, arguments);
- }
-
- PaymentlineCollection.prototype.model = Paymentline;
- return PaymentlineCollection;
- })();
- var Order = (function() {
- __extends(Order, Backbone.Model);
- function Order() {
- Order.__super__.constructor.apply(this, arguments);
- }
-
- Order.prototype.defaults = {
- validated: false,
- step: 'products',
- };
- Order.prototype.initialize = function() {
- this.set({creationDate: new Date});
- this.set({
- orderLines: new OrderlineCollection
- });
- this.set({
- paymentLines: new PaymentlineCollection
- });
- this.bind('change:validated', this.validatedChanged);
- return this.set({
- name: "Order " + this.generateUniqueId()
- });
- };
- Order.prototype.events = {
- 'change:validated': 'validatedChanged'
- };
- Order.prototype.validatedChanged = function() {
- if (this.get("validated") && !this.previous("validated")) {
- this.set({'step': 'receipt'});
- }
- }
- Order.prototype.generateUniqueId = function() {
- return new Date().getTime();
- };
- Order.prototype.addProduct = function(product) {
- var existing;
- existing = (this.get('orderLines')).get(product.id);
- if (existing != null) {
- existing.incrementQuantity();
- } else {
- var line = new Orderline(product.toJSON());
- this.get('orderLines').add(line);
- line.bind('killme', function() {
- this.get('orderLines').remove(line);
- }, this);
- }
- };
- Order.prototype.addPaymentLine = function(cashRegister) {
- var newPaymentline;
- newPaymentline = new Paymentline(cashRegister);
- /* TODO: Should be 0 for cash-like accounts */
- newPaymentline.set({
- amount: this.getDueLeft()
- });
- return (this.get('paymentLines')).add(newPaymentline);
- };
- Order.prototype.getName = function() {
- return this.get('name');
- };
- Order.prototype.getTotal = function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
- return sum + orderLine.getPriceWithTax();
- }), 0);
- };
- Order.prototype.getTotalTaxExcluded = function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
- return sum + orderLine.getPriceWithoutTax();
- }), 0);
- };
- Order.prototype.getTax = function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
- return sum + orderLine.getTax();
- }), 0);
- };
- Order.prototype.getPaidTotal = function() {
- return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
- return sum + paymentLine.getAmount();
- }), 0);
- };
- Order.prototype.getChange = function() {
- return this.getPaidTotal() - this.getTotal();
- };
- Order.prototype.getDueLeft = function() {
- return this.getTotal() - this.getPaidTotal();
- };
- Order.prototype.exportAsJSON = function() {
- var orderLines, paymentLines, result;
- orderLines = [];
- (this.get('orderLines')).each(_.bind( function(item) {
- return orderLines.push([0, 0, item.exportAsJSON()]);
- }, this));
- paymentLines = [];
- (this.get('paymentLines')).each(_.bind( function(item) {
- return paymentLines.push([0, 0, item.exportAsJSON()]);
- }, this));
- result = {
- name: this.getName(),
- amount_paid: this.getPaidTotal(),
- amount_total: this.getTotal(),
- amount_tax: this.getTax(),
- amount_return: this.getChange(),
- lines: orderLines,
- statement_ids: paymentLines
- };
- return result;
- };
- return Order;
- })();
- var OrderCollection = (function() {
- __extends(OrderCollection, Backbone.Collection);
- function OrderCollection() {
- OrderCollection.__super__.constructor.apply(this, arguments);
- }
-
- OrderCollection.prototype.model = Order;
- return OrderCollection;
- })();
- var Shop = (function() {
- __extends(Shop, Backbone.Model);
- function Shop() {
- Shop.__super__.constructor.apply(this, arguments);
- }
-
- Shop.prototype.initialize = function() {
- this.set({
- orders: new OrderCollection(),
- products: new ProductCollection()
- });
- this.set({
- cashRegisters: new CashRegisterCollection(pos.get('bank_statements')),
- });
- return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
- if ((this.get('orders')).isEmpty()) {
- this.addAndSelectOrder(new Order);
- }
- if ((this.get('selectedOrder')) === removedOrder) {
- return this.set({
- selectedOrder: (this.get('orders')).last()
- });
- }
- }, this));
- };
- Shop.prototype.addAndSelectOrder = function(newOrder) {
- (this.get('orders')).add(newOrder);
- return this.set({
- selectedOrder: newOrder
- });
- };
- return Shop;
- })();
- /*
- The numpad handles both the choice of the property currently being modified
- (quantity, price or discount) and the edition of the corresponding numeric value.
- */
- var NumpadState = Backbone.Model.extend({
- defaults: {
- buffer: "0",
- mode: "quantity"
- },
- appendNewChar: function(newChar) {
- var oldBuffer;
- oldBuffer = this.get('buffer');
- if (oldBuffer === '0') {
- this.set({
- buffer: newChar
- });
- } else if (oldBuffer === '-0') {
- this.set({
- buffer: "-" + newChar
- });
- } else {
- this.set({
- buffer: (this.get('buffer')) + newChar
- });
- }
- this.updateTarget();
- },
- deleteLastChar: function() {
- var tempNewBuffer;
- tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
- if (isNaN(tempNewBuffer)) {
- tempNewBuffer = "0";
- }
- this.set({
- buffer: tempNewBuffer
- });
- this.updateTarget();
- },
- switchSign: function() {
- var oldBuffer;
- oldBuffer = this.get('buffer');
- this.set({
- buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
- });
- this.updateTarget();
- },
- changeMode: function(newMode) {
- this.set({
- buffer: "0",
- mode: newMode
- });
- },
- reset: function() {
- this.set({
- buffer: "0",
- mode: "quantity"
- });
- },
- updateTarget: function() {
- var bufferContent, params;
- bufferContent = this.get('buffer');
- if (bufferContent && !isNaN(bufferContent)) {
- this.trigger('setValue', parseFloat(bufferContent));
- }
- },
- });
- /*
- ---
- Views
- ---
- */
- var NumpadWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.state = new NumpadState();
- },
- start: function() {
- this.state.bind('change:mode', this.changedMode, this);
- this.changedMode();
- this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
- this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
- this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
- this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
- },
- clickDeleteLastChar: function() {
- return this.state.deleteLastChar();
- },
- clickSwitchSign: function() {
- return this.state.switchSign();
- },
- clickAppendNewChar: function(event) {
- var newChar;
- newChar = event.currentTarget.innerText || event.currentTarget.textContent;
- return this.state.appendNewChar(newChar);
- },
- clickChangeMode: function(event) {
- var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
- return this.state.changeMode(newMode);
- },
- changedMode: function() {
- var mode = this.state.get('mode');
- $('.selected-mode').removeClass('selected-mode');
- $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
- },
- });
- /*
- Gives access to the payment methods (aka. 'cash registers')
- */
- var PaypadWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.shop = options.shop;
- },
- start: function() {
- this.$element.find('button').click(_.bind(this.performPayment, this));
- },
- performPayment: function(event) {
- if (this.shop.get('selectedOrder').get('step') === 'receipt')
- return;
- var cashRegister, cashRegisterCollection, cashRegisterId;
- /* set correct view */
- this.shop.get('selectedOrder').set({'step': 'payment'});
-
- cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue;
- cashRegisterCollection = this.shop.get('cashRegisters');
- cashRegister = cashRegisterCollection.find(_.bind( function(item) {
- return (item.get('id')) === parseInt(cashRegisterId, 10);
- }, this));
- return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister);
- },
- renderElement: function() {
- this.$element.empty();
- return (this.shop.get('cashRegisters')).each(_.bind( function(cashRegister) {
- var button = new PaymentButtonWidget();
- button.model = cashRegister;
- button.appendTo(this.$element);
- }, this));
- }
- });
- var PaymentButtonWidget = db.web.OldWidget.extend({
- template_fct: qweb_template('pos-payment-button-template'),
- renderElement: function() {
- this.$element.html(this.template_fct({
- id: this.model.get('id'),
- name: (this.model.get('journal_id'))[1]
- }));
- return this;
- }
- });
- /*
- There are 3 steps in a POS workflow:
- 1. prepare the order (i.e. chose products, quantities etc.)
- 2. choose payment method(s) and amount(s)
- 3. validae order and print receipt
- It should be possible to go back to any step as long as step 3 hasn't been completed.
- Modifying an order after validation shouldn't be allowed.
- */
- var StepSwitcher = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.shop = options.shop;
- this.change_order();
- this.shop.bind('change:selectedOrder', this.change_order, this);
- },
- change_order: function() {
- if (this.selected_order) {
- this.selected_order.unbind('change:step', this.change_step);
- }
- this.selected_order = this.shop.get('selectedOrder');
- if (this.selected_order) {
- this.selected_order.bind('change:step', this.change_step, this);
- }
- this.change_step();
- },
- change_step: function() {
- var new_step = this.selected_order ? this.selected_order.get('step') : 'products';
- $('.step-screen').hide();
- $('#' + new_step + '-screen').show();
- },
- });
- /*
- Shopping carts.
- */
- var OrderlineWidget = db.web.OldWidget.extend({
- tagName: 'tr',
- template_fct: qweb_template('pos-orderline-template'),
- init: function(parent, options) {
- this._super(parent);
- this.model = options.model;
- this.model.bind('change', _.bind( function() {
- this.refresh();
- }, this));
- this.model.bind('remove', _.bind( function() {
- this.$element.remove();
- }, this));
- this.order = options.order;
- },
- start: function() {
- this.$element.click(_.bind(this.clickHandler, this));
- this.refresh();
- },
- clickHandler: function() {
- this.select();
- },
- renderElement: function() {
- this.$element.html(this.template_fct(this.model.toJSON()));
- this.select();
- },
- refresh: function() {
- this.renderElement();
- var heights = _.map(this.$element.prevAll(), function(el) {return $(el).outerHeight();});
- heights.push($('#current-order thead').outerHeight());
- var position = _.reduce(heights, function(memo, num){ return memo + num; }, 0);
- $('#current-order').scrollTop(position);
- },
- select: function() {
- $('tr.selected').removeClass('selected');
- this.$element.addClass('selected');
- this.order.selected = this.model;
- this.on_selected();
- },
- on_selected: function() {},
- });
- var OrderWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.shop = options.shop;
- this.setNumpadState(options.numpadState);
- this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
- this.bindOrderLineEvents();
- },
- setNumpadState: function(numpadState) {
- if (this.numpadState) {
- this.numpadState.unbind('setValue', this.setValue);
- }
- this.numpadState = numpadState;
- if (this.numpadState) {
- this.numpadState.bind('setValue', this.setValue, this);
- this.numpadState.reset();
- }
- },
- setValue: function(val) {
- var param = {};
- param[this.numpadState.get('mode')] = val;
- var order = this.shop.get('selectedOrder');
- if (order.get('orderLines').length !== 0) {
- order.selected.set(param);
- } else {
- this.shop.get('selectedOrder').destroy();
- }
- },
- changeSelectedOrder: function() {
- this.currentOrderLines.unbind();
- this.bindOrderLineEvents();
- this.renderElement();
- },
- bindOrderLineEvents: function() {
- this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
- this.currentOrderLines.bind('add', this.addLine, this);
- this.currentOrderLines.bind('remove', this.renderElement, this);
- },
- addLine: function(newLine) {
- var line = new OrderlineWidget(null, {
- model: newLine,
- order: this.shop.get('selectedOrder')
- });
- line.on_selected.add(_.bind(this.selectedLine, this));
- this.selectedLine();
- line.appendTo(this.$element);
- this.updateSummary();
- },
- selectedLine: function() {
- var reset = false;
- if (this.currentSelected !== this.shop.get('selectedOrder').selected) {
- reset = true;
- }
- this.currentSelected = this.shop.get('selectedOrder').selected;
- if (reset && this.numpadState)
- this.numpadState.reset();
- this.updateSummary();
- },
- renderElement: function() {
- this.$element.empty();
- this.currentOrderLines.each(_.bind( function(orderLine) {
- var line = new OrderlineWidget(null, {
- model: orderLine,
- order: this.shop.get('selectedOrder')
- });
- line.on_selected.add(_.bind(this.selectedLine, this));
- line.appendTo(this.$element);
- }, this));
- this.updateSummary();
- },
- updateSummary: function() {
- var currentOrder, tax, total, totalTaxExcluded;
- currentOrder = this.shop.get('selectedOrder');
- total = currentOrder.getTotal();
- totalTaxExcluded = currentOrder.getTotalTaxExcluded();
- tax = currentOrder.getTax();
- $('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
- $('#tax').html(tax.toFixed(2)).hide().fadeIn();
- $('#total').html(total.toFixed(2)).hide().fadeIn();
- },
- });
- /*
- "Products" step.
- */
- var CategoryWidget = db.web.OldWidget.extend({
- start: function() {
- this.$element.find(".oe_pos_categories_list a").click(_.bind(this.changeCategory, this));
- },
- template_fct: qweb_template('pos-category-template'),
- renderElement: function() {
- var self = this;
- var c;
- this.$element.html(this.template_fct({
- breadcrumb: (function() {
- var _i, _len, _results;
- _results = [];
- for (_i = 0, _len = self.ancestors.length; _i < _len; _i++) {
- c = self.ancestors[_i];
- _results.push(pos.categories[c]);
- }
- return _results;
- })(),
- categories: (function() {
- var _i, _len, _results;
- _results = [];
- for (_i = 0, _len = self.children.length; _i < _len; _i++) {
- c = self.children[_i];
- _results.push(pos.categories[c]);
- }
- return _results;
- })()
- }));
- },
- changeCategory: function(a) {
- var id = $(a.target).data("category-id");
- this.on_change_category(id);
- },
- on_change_category: function(id) {},
- });
- var ProductWidget = db.web.OldWidget.extend({
- tagName:'li',
- template_fct: qweb_template('pos-product-template'),
- init: function(parent, options) {
- this._super(parent);
- this.model = options.model;
- this.shop = options.shop;
- },
- start: function(options) {
- $("a", this.$element).click(_.bind(this.addToOrder, this));
- },
- addToOrder: function(event) {
- /* Preserve the category URL */
- event.preventDefault();
- return (this.shop.get('selectedOrder')).addProduct(this.model);
- },
- renderElement: function() {
- this.$element.addClass("product");
- this.$element.html(this.template_fct(this.model.toJSON()));
- return this;
- },
- });
- var ProductListWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.model = options.model;
- this.shop = options.shop;
- this.shop.get('products').bind('reset', this.renderElement, this);
- },
- renderElement: function() {
- this.$element.empty();
- (this.shop.get('products')).each(_.bind( function(product) {
- var p = new ProductWidget(null, {
- model: product,
- shop: this.shop
- });
- p.appendTo(this.$element);
- }, this));
- return this;
- },
- });
- /*
- "Payment" step.
- */
- var PaymentlineWidget = db.web.OldWidget.extend({
- tagName: 'tr',
- template_fct: qweb_template('pos-paymentline-template'),
- init: function(parent, options) {
- this._super(parent);
- this.model = options.model;
- this.model.bind('change', this.changedAmount, this);
- },
- on_delete: function() {},
- changeAmount: function(event) {
- var newAmount;
- newAmount = event.currentTarget.value;
- if (newAmount && !isNaN(newAmount)) {
- this.amount = parseFloat(newAmount);
- this.model.set({
- amount: this.amount,
- });
- }
- },
- changedAmount: function() {
- if (this.amount !== this.model.get('amount'))
- this.renderElement();
- },
- renderElement: function() {
- this.amount = this.model.get('amount');
- this.$element.html(this.template_fct({
- name: (this.model.get('journal_id'))[1],
- amount: this.amount,
- }));
- this.$element.addClass('paymentline');
- $('input', this.$element).keyup(_.bind(this.changeAmount, this));
- $('.delete-payment-line', this.$element).click(this.on_delete);
- },
- });
- var PaymentWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.model = options.model;
- this.shop = options.shop;
- this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
- this.bindPaymentLineEvents();
- this.bindOrderLineEvents();
- },
- paymentLineList: function() {
- return this.$element.find('#paymentlines');
- },
- start: function() {
- $('button#validate-order', this.$element).click(_.bind(this.validateCurrentOrder, this));
- $('.oe_back_to_products', this.$element).click(_.bind(this.back, this));
- },
- back: function() {
- this.shop.get('selectedOrder').set({"step": "products"});
- },
- validateCurrentOrder: function() {
- var callback, currentOrder;
- currentOrder = this.shop.get('selectedOrder');
- $('button#validate-order', this.$element).attr('disabled', 'disabled');
- pos.pushOrder(currentOrder.exportAsJSON()).then(_.bind(function() {
- $('button#validate-order', this.$element).removeAttr('disabled');
- return currentOrder.set({
- validated: true
- });
- }, this));
- },
- bindPaymentLineEvents: function() {
- this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
- this.currentPaymentLines.bind('add', this.addPaymentLine, this);
- this.currentPaymentLines.bind('remove', this.renderElement, this);
- this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
- },
- bindOrderLineEvents: function() {
- this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
- this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
- },
- changeSelectedOrder: function() {
- this.currentPaymentLines.unbind();
- this.bindPaymentLineEvents();
- this.currentOrderLines.unbind();
- this.bindOrderLineEvents();
- this.renderElement();
- },
- addPaymentLine: function(newPaymentLine) {
- var x = new PaymentlineWidget(null, {
- model: newPaymentLine
- });
- x.on_delete.add(_.bind(this.deleteLine, this, x));
- x.appendTo(this.paymentLineList());
- },
- renderElement: function() {
- this.paymentLineList().empty();
- this.currentPaymentLines.each(_.bind( function(paymentLine) {
- this.addPaymentLine(paymentLine);
- }, this));
- this.updatePaymentSummary();
- },
- deleteLine: function(lineWidget) {
- this.currentPaymentLines.remove([lineWidget.model]);
- },
- updatePaymentSummary: function() {
- var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
- currentOrder = this.shop.get('selectedOrder');
- paidTotal = currentOrder.getPaidTotal();
- dueTotal = currentOrder.getTotal();
- this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
- this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
- remainingAmount = dueTotal - paidTotal;
- remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
- $('#payment-remaining').html(remaining);
- },
- setNumpadState: function(numpadState) {
- if (this.numpadState) {
- this.numpadState.unbind('setValue', this.setValue);
- this.numpadState.unbind('change:mode', this.setNumpadMode);
- }
- this.numpadState = numpadState;
- if (this.numpadState) {
- this.numpadState.bind('setValue', this.setValue, this);
- this.numpadState.bind('change:mode', this.setNumpadMode, this);
- this.numpadState.reset();
- this.setNumpadMode();
- }
- },
- setNumpadMode: function() {
- this.numpadState.set({mode: 'payment'});
- },
- setValue: function(val) {
- this.currentPaymentLines.last().set({amount: val});
- },
- });
- var ReceiptWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.model = options.model;
- this.shop = options.shop;
- this.user = pos.get('user');
- this.company = pos.get('company');
- this.shop_obj = pos.get('shop');
- },
- start: function() {
- this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
- this.changeSelectedOrder();
- },
- renderElement: function() {
- this.$element.html(qweb_template('pos-receipt-view'));
- $('button#pos-finish-order', this.$element).click(_.bind(this.finishOrder, this));
- $('button#print-the-ticket', this.$element).click(_.bind(this.print, this));
- },
- print: function() {
- window.print();
- },
- finishOrder: function() {
- this.shop.get('selectedOrder').destroy();
- },
- changeSelectedOrder: function() {
- if (this.currentOrderLines)
- this.currentOrderLines.unbind();
- this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
- this.currentOrderLines.bind('add', this.refresh, this);
- this.currentOrderLines.bind('change', this.refresh, this);
- this.currentOrderLines.bind('remove', this.refresh, this);
- if (this.currentPaymentLines)
- this.currentPaymentLines.unbind();
- this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
- this.currentPaymentLines.bind('all', this.refresh, this);
- this.refresh();
- },
- refresh: function() {
- this.currentOrder = this.shop.get('selectedOrder');
- $('.pos-receipt-container', this.$element).html(qweb_template('pos-ticket')({widget:this}));
- },
- });
- var OrderButtonWidget = db.web.OldWidget.extend({
- tagName: 'li',
- template_fct: qweb_template('pos-order-selector-button-template'),
- init: function(parent, options) {
- this._super(parent);
- this.order = options.order;
- this.shop = options.shop;
- this.order.bind('destroy', _.bind( function() {
- this.destroy();
- }, this));
- this.shop.bind('change:selectedOrder', _.bind( function(shop) {
- var selectedOrder;
- selectedOrder = shop.get('selectedOrder');
- if (this.order === selectedOrder) {
- this.setButtonSelected();
- }
- }, this));
- },
- start: function() {
- $('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
- $('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
- },
- selectOrder: function(event) {
- this.shop.set({
- selectedOrder: this.order
- });
- },
- setButtonSelected: function() {
- $('.selected-order').removeClass('selected-order');
- this.$element.addClass('selected-order');
- },
- closeOrder: function(event) {
- this.order.destroy();
- },
- renderElement: function() {
- this.$element.html(this.template_fct({widget:this}));
- this.$element.addClass('order-selector-button');
- }
- });
- var ShopWidget = db.web.OldWidget.extend({
- init: function(parent, options) {
- this._super(parent);
- this.shop = options.shop;
- },
- start: function() {
- $('button#neworder-button', this.$element).click(_.bind(this.createNewOrder, this));
-
- (this.shop.get('orders')).bind('add', this.orderAdded, this);
- (this.shop.get('orders')).add(new Order);
- this.productListView = new ProductListWidget(null, {
- shop: this.shop
- });
- this.productListView.$element = $("#products-screen-ol");
- this.productListView.renderElement();
- this.productListView.start();
- this.paypadView = new PaypadWidget(null, {
- shop: this.shop
- });
- this.paypadView.$element = $('#paypad');
- this.paypadView.renderElement();
- this.paypadView.start();
- this.numpadView = new NumpadWidget(null);
- this.numpadView.$element = $('#numpad');
- this.numpadView.start();
- this.orderView = new OrderWidget(null, {
- shop: this.shop,
- });
- this.orderView.$element = $('#current-order-content');
- this.orderView.start();
- this.paymentView = new PaymentWidget(null, {
- shop: this.shop
- });
- this.paymentView.$element = $('#payment-screen');
- this.paymentView.renderElement();
- this.paymentView.start();
- this.receiptView = new ReceiptWidget(null, {
- shop: this.shop,
- });
- this.receiptView.replace($('#receipt-screen'));
- this.stepSwitcher = new StepSwitcher(this, {shop: this.shop});
- this.shop.bind('change:selectedOrder', this.changedSelectedOrder, this);
- this.changedSelectedOrder();
- },
- createNewOrder: function() {
- var newOrder;
- newOrder = new Order;
- (this.shop.get('orders')).add(newOrder);
- this.shop.set({
- selectedOrder: newOrder
- });
- },
- orderAdded: function(newOrder) {
- var newOrderButton;
- newOrderButton = new OrderButtonWidget(null, {
- order: newOrder,
- shop: this.shop
- });
- newOrderButton.appendTo($('#orders'));
- newOrderButton.selectOrder();
- },
- changedSelectedOrder: function() {
- if (this.currentOrder) {
- this.currentOrder.unbind('change:step', this.changedStep);
- }
- this.currentOrder = this.shop.get('selectedOrder');
- this.currentOrder.bind('change:step', this.changedStep, this);
- this.changedStep();
- },
- changedStep: function() {
- var step = this.currentOrder.get('step');
- this.orderView.setNumpadState(null);
- this.paymentView.setNumpadState(null);
- if (step === 'products') {
- this.orderView.setNumpadState(this.numpadView.state);
- } else if (step === 'payment') {
- this.paymentView.setNumpadState(this.numpadView.state);
- }
- },
- });
- var App = (function() {
- function App($element) {
- this.initialize($element);
- }
-
- App.prototype.initialize = function($element) {
- this.shop = new Shop;
- this.shopView = new ShopWidget(null, {
- shop: this.shop
- });
- this.shopView.$element = $element;
- this.shopView.start();
- this.categoryView = new CategoryWidget(null, 'products-screen-categories');
- this.categoryView.on_change_category.add_last(_.bind(this.category, this));
- this.category();
- };
- App.prototype.category = function(id) {
- var c, product_list;
- if (id == null) {
- id = 0;
- }
- c = pos.categories[id];
- this.categoryView.ancestors = c.ancestors;
- this.categoryView.children = c.children;
- this.categoryView.renderElement();
- this.categoryView.start();
- product_list = pos.get('product_list').filter( function(p) {
- var _ref;
- return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
- });
- (this.shop.get('products')).reset(product_list);
- var self = this;
- $('.searchbox input').keyup(function() {
- var m, s;
- s = $(this).val().toLowerCase();
- if (s) {
- m = product_list.filter( function(p) {
- return p.name.toLowerCase().indexOf(s) != -1;
- });
- $('.search-clear').fadeIn();
- } else {
- m = product_list;
- $('.search-clear').fadeOut();
- }
- return (self.shop.get('products')).reset(m);
- });
- return $('.search-clear').click( function() {
- (self.shop.get('products')).reset(product_list);
- $('.searchbox input').val('').focus();
- return $('.search-clear').fadeOut();
- });
- };
- return App;
- })();
-
- db.point_of_sale.SynchNotification = db.web.OldWidget.extend({
- template: "pos-synch-notification",
- init: function() {
- this._super.apply(this, arguments);
- this.nbr_pending = 0;
- },
- renderElement: function() {
- this._super.apply(this, arguments);
- $('.oe_pos_synch-notification-button', this.$element).click(this.on_synch);
- },
- on_change_nbr_pending: function(nbr_pending) {
- this.nbr_pending = nbr_pending;
- this.renderElement();
- },
- on_synch: function() {}
- });
-
- db.web.client_actions.add('pos.ui', 'db.point_of_sale.PointOfSale');
- db.point_of_sale.PointOfSale = db.web.OldWidget.extend({
- init: function() {
- this._super.apply(this, arguments);
-
- if (pos)
- throw "It is not possible to instantiate multiple instances "+
- "of the point of sale at the same time.";
- pos = new Pos(this.session);
- },
- start: function() {
- var self = this;
- return pos.ready.then(_.bind(function() {
- this.renderElement();
- this.synch_notification = new db.point_of_sale.SynchNotification(this);
- this.synch_notification.replace($('.oe_pos_synch-notification', this.$element));
- this.synch_notification.on_synch.add(_.bind(pos.flush, pos));
-
- pos.bind('change:nbr_pending_operations', this.changed_pending_operations, this);
- this.changed_pending_operations();
-
- this.$element.find("#loggedas button").click(function() {
- self.try_close();
- });
-
- pos.app = new App(self.$element);
- db.webclient.set_content_full_screen(true);
-
- if (pos.get('bank_statements').length === 0)
- return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_open_statement']], ['res_id']).pipe(
- _.bind(function(res) {
- return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
- var action = result.result;
- this.do_action(action);
- }, this));
- }, this));
- }, this));
- },
- render: function() {
- return qweb_template("PointOfSale")();
- },
- changed_pending_operations: function () {
- this.synch_notification.on_change_nbr_pending(pos.get('nbr_pending_operations'));
- },
- try_close: function() {
- pos.flush().then(_.bind(function() {
- var close = _.bind(this.close, this);
- if (pos.get('nbr_pending_operations') > 0) {
- var confirm = false;
- $(QWeb.render('pos-close-warning')).dialog({
- resizable: false,
- height:160,
- modal: true,
- title: "Warning",
- buttons: {
- "Yes": function() {
- confirm = true;
- $( this ).dialog( "close" );
- },
- "No": function() {
- $( this ).dialog( "close" );
- }
- },
- close: function() {
- if (confirm)
- close();
- }
- });
- } else {
- close();
- }
- }, this));
- },
- close: function() {
- return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
- _.bind(function(res) {
- return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
- var action = result.result;
- action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'default_home'}});
- this.do_action(action);
- }, this));
- }, this));
- },
- destroy: function() {
- db.webclient.set_content_full_screen(false);
- pos = undefined;
- this._super();
- }
- });
-}
diff --git a/addons/point_of_sale/static/src/js/pos_basewidget.js b/addons/point_of_sale/static/src/js/pos_basewidget.js
new file mode 100644
index 00000000000..62097f39db3
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_basewidget.js
@@ -0,0 +1,45 @@
+function openerp_pos_basewidget(instance, module){ //module is instance.point_of_sale
+
+ // This is a base class for all Widgets in the POS. It exposes relevant data to the
+ // templates :
+ // - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after }
+ // - widget.format_currency(amount) : this method returns a formatted string based on the
+ // symbol, the position, and the amount of money.
+ // if the PoS is not fully loaded when you instanciate the widget, the currency might not
+ // yet have been initialized. Use __build_currency_template() to recompute with correct values
+ // before rendering.
+
+ module.PosBaseWidget = instance.web.Widget.extend({
+ init:function(parent,options){
+ this._super(parent);
+ options = options || {};
+ this.pos = options.pos || (parent ? parent.pos : undefined);
+ this.pos_widget = options.pos_widget || (parent ? parent.pos_widget : undefined);
+ this.build_currency_template();
+ },
+ build_currency_template: function(){
+
+ if(this.pos && this.pos.get('currency')){
+ this.currency = this.pos.get('currency');
+ }else{
+ this.currency = {symbol: '$', position: 'after'};
+ }
+
+ this.format_currency = function(amount){
+ if(this.currency.position === 'after'){
+ return Math.round(amount*100)/100 + ' ' + this.currency.symbol;
+ }else{
+ return this.currency.symbol + ' ' + Math.round(amount*100)/100;
+ }
+ }
+
+ },
+ show: function(){
+ this.$element.show();
+ },
+ hide: function(){
+ this.$element.hide();
+ },
+ });
+
+}
diff --git a/addons/point_of_sale/static/src/js/pos_devices.js b/addons/point_of_sale/static/src/js/pos_devices.js
new file mode 100644
index 00000000000..2857fc39937
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_devices.js
@@ -0,0 +1,432 @@
+
+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'; },
+ }))();
+
+ //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.
+
+ module.ProxyDevice = instance.web.Class.extend({
+ init: function(options){
+ options = options || {};
+ url = options.url || 'http://localhost:8069';
+
+ this.weight = 0;
+ this.weighting = false;
+
+ this.paying = false;
+ this.payment_status = 'waiting_for_payment';
+
+ this.connection = new instance.web.JsonRPC();
+ this.connection.setup(url);
+
+ },
+ message : function(name,params,success_callback, error_callback){
+ success_callback = success_callback || function(){};
+ error_callback = error_callback || function(){};
+
+ if(debug_devices && debug_devices.active){
+ console.log('PROXY:',name,params);
+ }else{
+ this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback);
+ }
+ },
+
+ //a product has been scanned and recognized with success
+ scan_item_success: function(){
+ this.message('scan_item_success');
+ },
+
+ //a product has been scanned but not recognized
+ scan_item_error_unrecognized: function(){
+ this.message('scan_item_error_unrecognized');
+ },
+
+ //the client is asking for help
+ help_needed: function(){
+ this.message('help_needed');
+ },
+
+ //the client does not need help anymore
+ help_canceled: function(){
+ this.message('help_canceled');
+ },
+
+ //the client is starting to weight
+ weighting_start: function(){
+ this.weight = 0;
+ if(debug_devices){
+ debug_devices.weigth = 0;
+ }
+ this.weighting = true;
+ this.message('weighting_start');
+ },
+
+ //returns the weight on the scale.
+ // is called at regular interval (up to 10x/sec) between a weighting_start()
+ // and a weighting_end()
+ weighting_read_kg: function(){
+ var self = this;
+ if(debug_devices && debug_devices.active){
+ return debug_devices.weight;
+ }else{
+ this.message('weighting_read_kg',{},function(weight){
+ if(self.weighting){
+ self.weight = weight;
+ }
+ });
+ return self.weight;
+ }
+ },
+
+ // the client has finished weighting products
+ weighting_end: function(){
+ this.weight = 0;
+ this.weighting = false;
+ this.message('weighting_end');
+ },
+
+ // the pos asks the client to pay 'price' units
+ // method: 'mastercard' | 'cash' | ... ? TBD
+ // info: 'extra information to display on the payment terminal' ... ? TBD
+ 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});
+ },
+
+ // is called at regular interval after a payment request to see if the client
+ // has paid the required money
+ // returns 'waiting_for_payment' | 'payment_accepted' | 'payment_rejected'
+ is_payment_accepted: function(){
+ var self = this;
+ if(debug_devices.active){
+ return debug_devices.payment_status;
+ }else{
+ this.message('is_payment_accepted', {}, function(payment_status){
+ if(self.paying){
+ self.payment_status = payment_status;
+ }
+ });
+ return self.payment_status;
+ }
+ },
+
+ // the client cancels his payment
+ payment_canceled: function(){
+ this.paying = false;
+ this.payment_status = 'waiting_for_payment';
+ this.message('payment_canceled');
+ },
+
+ // called when the client logs in or starts to scan product
+ transaction_start: function(){
+ this.message('transaction_start');
+ },
+
+ // called when the clients has finished his interaction with the machine
+ transaction_end: function(){
+ this.message('transaction_end');
+ },
+
+ // called when the POS turns to cashier mode
+ cashier_mode_activated: function(){
+ this.message('cashier_mode_activated');
+ },
+
+ // called when the POS turns to client mode
+ cashier_mode_deactivated: function(){
+ this.message('cashier_mode_deactivated');
+ },
+
+ // ask for the cashbox (the physical box where you store the cash) to be opened
+ open_cashbox: function(){
+ this.message('open_cashbox');
+ },
+
+ /* ask the printer to print a receipt
+ * receipt is a JSON object with the following specs:
+ * receipt{
+ * - orderlines : list of orderlines :
+ * {
+ * quantity: (number) the number of items, or the weight,
+ * unit_name: (string) the name of the item's unit (kg, dozen, ...)
+ * list_price: (number) the price of one unit of the item before discount
+ * discount: (number) the discount on the product in % [0,100]
+ * product_name: (string) the name of the product
+ * price_with_tax: (number) the price paid for this orderline, tax included
+ * price_without_tax: (number) the price paid for this orderline, without taxes
+ * tax: (number) the price paid in taxes on this orderline
+ * }
+ * - paymentlines : list of paymentlines :
+ * {
+ * amount: (number) the amount paid
+ * journal: (string) the name of the journal on wich the payment has been made
+ * }
+ * - total_with_tax: (number) the total of the receipt tax included
+ * - total_without_tax: (number) the total of the receipt without taxes
+ * - total_tax: (number) the total amount of taxes paid
+ * - total_paid: (number) the total sum paid by the client
+ * - change: (number) the amount of change given back to the client
+ * - name: (string) a unique name for this order
+ * - client: (string) name of the client. or null if no client is logged
+ * - cashier: (string) the name of the cashier
+ * - date: { the date at wich the payment has been done
+ * year: (number) the year [2012, ...]
+ * month: (number) the month [0,11]
+ * date: (number) the day of the month [1,31]
+ * day: (number) the day of the week [0,6]
+ * hour: (number) the hour [0,23]
+ * minute: (number) the minute [0,59]
+ * }
+ */
+ print_receipt: function(receipt){
+ this.message('print_receipt',{receipt: receipt});
+ },
+ });
+
+ // this module interfaces with the barcode reader. It assumes the barcode reader
+ // is set-up to act like a keyboard. Use connect() and disconnect() to activate
+ // and deactivate the barcode reader. Use set_action_callbacks to tell it
+ // what to do when it reads a barcode.
+ module.BarcodeReader = instance.web.Class.extend({
+ actions:[
+ 'product',
+ 'cashier',
+ 'client',
+ 'discount',
+ ],
+ init: function(attributes){
+ this.pos = attributes.pos;
+ this.action_callback = {};
+
+ this.action_callback_stack = [];
+
+ this.price_prefix_set = attributes.price_prefix_set || {'02':'', '22':'', '24':'', '26':'', '28':''};
+ this.weight_prefix_set = attributes.weight_prefix_set || {'21':'','23':'','27':'','29':'','25':''};
+ this.client_prefix_set = attributes.weight_prefix_set || {'42':''};
+ this.cashier_prefix_set = attributes.weight_prefix_set || {'40':''};
+ this.discount_prefix_set = attributes.weight_prefix_set || {'44':''};
+ },
+ save_callbacks: function(){
+ var callbacks = {};
+ for(name in this.action_callback){
+ callbacks[name] = this.action_callback[name];
+ }
+ this.action_callback_stack.push(callbacks);
+ },
+ restore_callbacks: function(){
+ if(this.action_callback_stack.length){
+ var callbacks = this.action_callback_stack.pop();
+ this.action_callback = callbacks;
+ }
+ },
+
+ // when an ean is scanned and parsed, the callback corresponding
+ // to its type is called with the parsed_ean as a parameter.
+ // (parsed_ean is the result of parse_ean(ean))
+ //
+ // callbacks is a Map of 'actions' : callback(parsed_ean)
+ // that sets the callback for each action. if a callback for the
+ // specified action already exists, it is replaced.
+ //
+ // possible actions include :
+ // 'product' | 'cashier' | 'client' | 'discount'
+
+ set_action_callback: function(action, callback){
+ if(arguments.length == 2){
+ this.action_callback[action] = callback;
+ }else{
+ var actions = arguments[0];
+ for(action in actions){
+ this.set_action_callback(action,actions[action]);
+ }
+ }
+ },
+
+ //remove all action callbacks
+ reset_action_callbacks: function(){
+ for(action in this.action_callback){
+ this.action_callback[action] = undefined;
+ }
+ },
+
+
+ // returns true if the ean is a valid EAN codebar number by checking the control digit.
+ // ean must be a string
+ check_ean: function(ean){
+ var code = ean.split('');
+ for(var i = 0; i < code.length; i++){
+ code[i] = Number(code[i]);
+ }
+ var st1 = code.slice();
+ var st2 = st1.slice(0,st1.length-1).reverse();
+ // some EAN13 barcodes have a length of 12, as they start by 0
+ while (st2.length < 12) {
+ st2.push(0);
+ }
+ var countSt3 = 1;
+ var st3 = 0;
+ $.each(st2, function() {
+ if (countSt3%2 === 1) {
+ st3 += this;
+ }
+ countSt3 ++;
+ });
+ st3 *= 3;
+ var st4 = 0;
+ var countSt4 = 1;
+ $.each(st2, function() {
+ if (countSt4%2 === 0) {
+ st4 += this;
+ }
+ countSt4 ++;
+ });
+ var st5 = st3 + st4;
+ var cd = (10 - (st5%10)) % 10;
+ return code[code.length-1] === cd;
+ },
+
+ // attempts to interpret an ean (string encoding an ean)
+ // it will check its validity then return an object containing various
+ // information about the ean.
+ // most importantly :
+ // - ean : the ean
+ // - type : the type of the ean:
+ // 'price' | 'weight' | 'unit' | 'cashier' | 'client' | 'discount' | 'error'
+ //
+ // - prefix : the prefix that has ben used to determine the type
+ // - id : the part of the ean that identifies something
+ // - value : if the id encodes a numerical value, it will be put there
+ // - unit : if the encoded value has a unit, it will be put there.
+ // not to be confused with the 'unit' type, which represent an unit of a
+ // unique product
+
+ parse_ean: function(ean){
+ var parse_result = {
+ type:'unknown', //
+ prefix:'',
+ ean:ean,
+ id:'',
+ value: 0,
+ unit: 'none',
+ };
+ var prefix2 = ean.substring(0,2);
+
+ if(!this.check_ean(ean)){
+ parse_result.type = 'error';
+ }else if (prefix2 in this.price_prefix_set){
+ parse_result.type = 'price';
+ parse_result.prefix = prefix2;
+ parse_result.id = ean.substring(0,7);
+ parse_result.value = Number(ean.substring(7,12))/100.0;
+ parse_result.unit = 'euro';
+ } else if (prefix2 in this.weight_prefix_set){
+ parse_result.type = 'weight';
+ parse_result.prefix = prefix2;
+ parse_result.id = ean.substring(0,7);
+ parse_result.value = Number(ean.substring(7,12))/1000.0;
+ parse_result.unit = 'Kg';
+ }else if (prefix2 in this.client_prefix_set){
+ parse_result.type = 'client';
+ parse_result.prefix = prefix2;
+ parse_result.id = ean.substring(0,7);
+ }else if (prefix2 in this.cashier_prefix_set){
+ parse_result.type = 'cashier';
+ parse_result.prefix = prefix2;
+ parse_result.id = ean.substring(0,7);
+ }else if (prefix2 in this.discount_prefix_set){
+ parse_result.type = 'discount';
+ parse_result.prefix = prefix2;
+ parse_result.id = ean.substring(0,7);
+ parse_result.value = Number(ean.substring(7,12))/100.0;
+ parse_result.unit = '%';
+ }else{
+ parse_result.type = 'unit';
+ parse_result.prefix = '';
+ parse_result.id = ean;
+ }
+ return parse_result;
+ },
+
+ // starts catching keyboard events and tries to interpret codebar
+ // calling the callbacks when needed.
+ connect: function(){
+ var self = this;
+ var codeNumbers = [];
+ var timeStamp = 0;
+ var lastTimeStamp = 0;
+
+ // The barcode readers acts as a keyboard, we catch all keyup events and try to find a
+ // barcode sequence in the typed keys, then act accordingly.
+ $('body').delegate('','keyup', function (e){
+
+ //We only care about numbers
+ if (!isNaN(Number(String.fromCharCode(e.keyCode)))) {
+
+ // The barcode reader sends keystrokes with a specific interval.
+ // We look if the typed keys fit in the interval.
+ if (codeNumbers.length==0) {
+ timeStamp = new Date().getTime();
+ } else {
+ if (lastTimeStamp + 30 < new Date().getTime()) {
+ // not a barcode reader
+ codeNumbers = [];
+ timeStamp = new Date().getTime();
+ }
+ }
+ codeNumbers.push(e.keyCode - 48);
+ 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.error('ERROR: 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);
+ }
+ }
+
+ codeNumbers = [];
+ }
+ } else {
+ // NaN
+ codeNumbers = [];
+ }
+ });
+ },
+
+ // stops catching keyboard events
+ disconnect: function(){
+ $('body').undelegate('', 'keyup')
+ },
+ });
+
+}
diff --git a/addons/point_of_sale/static/src/js/pos_keyboard_widget.js b/addons/point_of_sale/static/src/js/pos_keyboard_widget.js
new file mode 100644
index 00000000000..2e683edb1e1
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_keyboard_widget.js
@@ -0,0 +1,185 @@
+
+function openerp_pos_keyboard(instance, module){ //module is instance.point_of_sale
+// ---------- OnScreen Keyboard Widget ----------
+
+ // A Widget that displays an onscreen keyboard.
+ // There are two options when creating the widget :
+ //
+ // * 'keyboard_model' : 'simple' | 'full' (default)
+ // The 'full' emulates a PC keyboard, while 'simple' emulates an 'android' one.
+ //
+ // * 'input_selector : (default: '.searchbox input')
+ // defines the dom element that the keyboard will write to.
+ //
+ // The widget is initially hidden. It can be shown with this.show(), and is
+ // automatically shown when the input_selector gets focused.
+
+ module.OnscreenKeyboardWidget = instance.web.Widget.extend({
+ template: 'OnscreenKeyboardSimple',
+ init: function(parent, options){
+ var self = this;
+ this._super(parent,options);
+ options = options || {};
+
+ this.keyboard_model = options.keyboard_model || 'full';
+ if(this.keyboard_model === 'full'){
+ this.template = 'OnscreenKeyboardFull';
+ }
+
+ this.input_selector = options.input_selector || '.searchbox input';
+
+ //show the keyboard when the input zone is clicked.
+ $(this.input_selector).focus(function(){self.show();});
+
+ //Keyboard state
+ this.capslock = false;
+ this.shift = false;
+ this.numlock = false;
+ },
+
+ connect : function(){
+ var self = this;
+ $(this.input_selector).focus(function(){self.show();});
+ },
+
+ // Write a character to the input zone
+ writeCharacter: function(character){
+ var $input = $(this.input_selector);
+ $input[0].value += character;
+ $input.keydown();
+ $input.keyup();
+ },
+
+ // Sends a 'return' character to the input zone. TODO
+ sendReturn: function(){
+ },
+
+ // Removes the last character from the input zone.
+ deleteCharacter: function(){
+ var $input = $(this.input_selector);
+ var input_value = $input[0].value;
+ $input[0].value = input_value.substr(0, input_value.length - 1);
+ $input.keydown();
+ $input.keyup();
+ },
+
+ // Clears the content of the input zone.
+ deleteAllCharacters: function(){
+ var $input = $(this.input_selector);
+ $input[0].value = "";
+ $input.keydown();
+ $input.keyup();
+ },
+
+ // Makes the keyboard show and slide from the bottom of the screen.
+ show: function(){
+ $('.keyboard_frame').show().animate({'height':'235px'}, 500, 'swing');
+ },
+
+ // Makes the keyboard hide by sliding to the bottom of the screen.
+ hide: function(){
+ var self = this;
+ var frame = $('.keyboard_frame');
+ frame.animate({'height':'0'}, 500, 'swing', function(){ frame.hide(); self.reset(); });
+ },
+
+ //What happens when the shift key is pressed : toggle case, remove capslock
+ toggleShift: function(){
+ $('.letter').toggleClass('uppercase');
+ $('.symbol span').toggle();
+
+ self.shift = (self.shift === true) ? false : true;
+ self.capslock = false;
+ },
+
+ //what happens when capslock is pressed : toggle case, set capslock
+ toggleCapsLock: function(){
+ $('.letter').toggleClass('uppercase');
+ self.capslock = true;
+ },
+
+ //What happens when numlock is pressed : toggle symbols and numlock label
+ toggleNumLock: function(){
+ $('.symbol span').toggle();
+ $('.numlock span').toggle();
+ self.numlock = (self.numlock === true ) ? false : true;
+ },
+
+ //After a key is pressed, shift is disabled.
+ removeShift: function(){
+ if (self.shift === true) {
+ $('.symbol span').toggle();
+ if (this.capslock === false) $('.letter').toggleClass('uppercase');
+
+ self.shift = false;
+ }
+ },
+
+ // Resets the keyboard to its original state; capslock: false, shift: false, numlock: false
+ reset: function(){
+ if(this.shift){
+ this.toggleShift();
+ }
+ if(this.capslock){
+ this.toggleCapsLock();
+ }
+ if(this.numlock){
+ this.toggleNumLock();
+ }
+ },
+
+ //called after the keyboard is in the DOM, sets up the key bindings.
+ start: function(){
+ var self = this;
+
+ //this.show();
+
+
+ $('.close_button').click(function(){
+ self.deleteAllCharacters();
+ self.hide();
+ });
+
+ // Keyboard key click handling
+ $('.keyboard li').click(function(){
+
+ var $this = $(this),
+ character = $this.html(); // If it's a lowercase letter, nothing happens to this variable
+
+ if ($this.hasClass('left-shift') || $this.hasClass('right-shift')) {
+ self.toggleShift();
+ return false;
+ }
+
+ if ($this.hasClass('capslock')) {
+ self.toggleCapsLock();
+ return false;
+ }
+
+ if ($this.hasClass('delete')) {
+ self.deleteCharacter();
+ return false;
+ }
+
+ if ($this.hasClass('numlock')){
+ self.toggleNumLock();
+ return false;
+ }
+
+ // Special characters
+ if ($this.hasClass('symbol')) character = $('span:visible', $this).html();
+ if ($this.hasClass('space')) character = ' ';
+ if ($this.hasClass('tab')) character = "\t";
+ if ($this.hasClass('return')) character = "\n";
+
+ // Uppercase letter
+ if ($this.hasClass('uppercase')) character = character.toUpperCase();
+
+ // Remove shift once a key is clicked.
+ self.removeShift();
+
+ self.writeCharacter(character);
+ });
+ },
+ });
+}
diff --git a/addons/point_of_sale/static/src/js/pos_main.js b/addons/point_of_sale/static/src/js/pos_main.js
new file mode 100644
index 00000000000..71ac69c6067
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_main.js
@@ -0,0 +1,25 @@
+
+openerp.point_of_sale = function(instance) {
+
+ instance.point_of_sale = {};
+
+ var module = instance.point_of_sale;
+
+ openerp_pos_models(instance,module); // import pos_models.js
+
+ openerp_pos_basewidget(instance,module); // import pos_basewidget.js
+
+ openerp_pos_keyboard(instance,module); // import pos_keyboard_widget.js
+
+ openerp_pos_scrollbar(instance,module); // import pos_scrollbar_widget.js
+
+ openerp_pos_screens(instance,module); // import pos_screens.js
+
+ openerp_pos_widgets(instance,module); // import pos_widgets.js
+
+ openerp_pos_devices(instance,module); // import pos_devices.js
+
+ instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
+};
+
+
diff --git a/addons/point_of_sale/static/src/js/pos_models.js b/addons/point_of_sale/static/src/js/pos_models.js
new file mode 100644
index 00000000000..fd1e3fb384f
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_models.js
@@ -0,0 +1,1026 @@
+function openerp_pos_models(instance, module){ //module is instance.point_of_sale
+ var QWeb = instance.web.qweb;
+
+ module.LocalStorageDAO = instance.web.Class.extend({
+ add_operation: function(operation) {
+ var self = this;
+ return $.async_when().pipe(function() {
+ var tmp = self._get('oe_pos_operations', []);
+ var last_id = self._get('oe_pos_operations_sequence', 1);
+ tmp.push({'id': last_id, 'data': operation});
+ self._set('oe_pos_operations', tmp);
+ self._set('oe_pos_operations_sequence', last_id + 1);
+ });
+ },
+ remove_operation: function(id) {
+ var self = this;
+ return $.async_when().pipe(function() {
+ var tmp = self._get('oe_pos_operations', []);
+ tmp = _.filter(tmp, function(el) {
+ return el.id !== id;
+ });
+ self._set('oe_pos_operations', tmp);
+ });
+ },
+ get_operations: function() {
+ var self = this;
+ return $.async_when().pipe(function() {
+ return self._get('oe_pos_operations', []);
+ });
+ },
+ _get: function(key, default_) {
+ var txt = localStorage['oe_pos_dao_'+key];
+ if (! txt)
+ return default_;
+ return JSON.parse(txt);
+ },
+ _set: function(key, value) {
+ localStorage['oe_pos_dao_'+key] = JSON.stringify(value);
+ },
+ reset_stored_data: function(){
+ for(key in localStorage){
+ if(key.indexOf('oe_pos_dao_') === 0){
+ delete localStorage[key];
+ }
+ }
+ },
+ });
+
+ 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 )
+ // it must contains a representation of the server's PoS backend.
+ // (taxes, product list, configuration options, etc.) this representation
+ // is fetched and stored by the PosModel at the initialisation.
+ // this is done asynchronously, a ready deferred alows the GUI to wait interactively
+ // for the loading to be completed
+ // There is a single instance of the PosModel for each Front-End instance, it is usually called
+ // 'pos' and is available to almost all widgets.
+
+ module.PosModel = Backbone.Model.extend({
+ initialize: function(session, attributes) {
+ Backbone.Model.prototype.initialize.call(this, attributes);
+ var self = this;
+ this.dao = new module.LocalStorageDAO(); // used to store the order's data on the Hard Drive
+ this.ready = $.Deferred(); // used to notify the GUI that the PosModel has loaded all resources
+ this.flush_mutex = new $.Mutex(); // used to make sure the orders are sent to the server once at time
+ //this.build_tree = _.bind(this.build_tree, this); // ???
+ this.session = session;
+ this.categories = {};
+ this.root_category = null;
+ this.weightable_categories = []; // a flat list of all categories that directly contain weightable products
+ this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes
+ this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
+
+ // pos settings
+ this.use_scale = false;
+ this.use_proxy_printer = false;
+ this.use_virtual_keyboard = false;
+ this.use_websql = false;
+ this.use_barcode_scanner = false;
+
+ // default attributes values. If null, it will be loaded below.
+ this.set({
+ 'nbr_pending_operations': 0,
+
+ 'currency': {symbol: '$', position: 'after'},
+ 'shop': null,
+ 'company': null,
+ 'user': null, // the user that loaded the pos
+ 'user_list': null, // list of all users
+ 'cashier': null, // the logged cashier, if different from user
+
+ 'orders': new module.OrderCollection(),
+ //this is the product list as seen by the product list widgets, it will change based on the category filters
+ 'products': new module.ProductCollection(),
+ 'cashRegisters': null,
+
+ 'product_list': null, // the list of all products, does not change.
+ 'bank_statements': null,
+ 'taxes': null,
+ 'pos_session': null,
+ 'pos_config': null,
+ 'categories': null,
+
+ 'selectedOrder': undefined,
+ });
+
+ this.get('orders').bind('remove', function(){ self.on_removed_order(); });
+
+ // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
+ // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched.
+
+ var user_def = fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
+ .pipe(function(users){
+ var user = users[0];
+ self.set('user',user);
+
+ return fetch('res.company',
+ [
+ 'currency_id',
+ 'email',
+ 'website',
+ 'company_registry',
+ //TODO contact_address
+ 'vat',
+ 'name',
+ 'phone'
+ ],
+ [['id','=',user.company_id[0]]])
+ }).pipe(function(companies){
+ var company = companies[0];
+ self.set('company',company);
+
+ return fetch('res.currency',['symbol','position'],[['id','=',company.currency_id[0]]]);
+ }).pipe(function (currencies){
+ self.set('currency',currencies[0]);
+ });
+
+ var cat_def = fetch('pos.category', ['id','name', 'parent_id', 'child_id', 'category_image_small'])
+ .pipe(function(result){
+ return self.set({'categories': result});
+ });
+
+ var uom_def = fetch( //unit of measure
+ 'product.uom',
+ null,
+ null
+ ).then(function(result){
+ self.set({'units': result});
+ var units_by_id = {};
+ for(var i = 0, len = result.length; i < len; i++){
+ units_by_id[result[i].id] = result[i];
+ }
+ self.set({'units_by_id':units_by_id});
+ });
+
+ var pack_def = fetch(
+ 'product.packaging',
+ null,
+ null
+ ).then(function(packaging){
+ self.set('product.packaging',packaging);
+ });
+
+ var users_def = fetch(
+ 'res.users',
+ ['name','ean13'],
+ [['ean13', '!=', false]]
+ ).then(function(result){
+ self.set({'user_list':result});
+ });
+
+ var tax_def = fetch('account.tax', ['amount','price_include','type'])
+ .then(function(result){
+ self.set({'taxes': result});
+ });
+
+ var session_def = fetch( // loading the PoS Session.
+ 'pos.session',
+ ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
+ [['state', '=', 'opened'], ['user_id', '=', this.session.uid]]
+ ).pipe(function(result) {
+
+ // some data are associated with the pos session, like the pos config and bank statements.
+ // we must have a valid session before we can read those.
+
+ var session_data_def = new $.Deferred();
+
+ if( result.length !== 0 ) {
+ var pos_session = result[0];
+
+ self.set({'pos_session': pos_session});
+
+ var pos_config_def = fetch(
+ 'pos.config',
+ ['name','journal_ids','shop_id','journal_id',
+ 'iface_self_checkout', 'iface_websql', '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','=', pos_session.config_id[0]]]
+ ).pipe(function(result){
+ var pos_config = result[0]
+
+ self.set({'pos_config': pos_config});
+ self.use_scale = pos_config.iface_electronic_scale || false;
+ self.use_proxy_printer = pos_config.iface_print_via_proxy || false;
+ self.use_virtual_keyboard = pos_config.iface_vkeyboard || false;
+ self.use_websql = pos_config.iface_websql || false;
+ self.use_barcode_scanner = pos_config.iface_barscan || false;
+ self.use_selfcheckout = pos_config.iface_self_checkout || false;
+ self.use_cashbox = pos_config.iface_cashdrawer || false;
+
+ return shop_def = fetch('sale.shop',[], [['id','=',pos_config.shop_id[0]]])
+ }).pipe(function(shops){
+ self.set('shop',shops[0]);
+ return fetch(
+ 'product.product',
+ //context {pricelist: shop.pricelist_id[0]}
+ ['name', 'list_price','price','pos_categ_id', 'taxes_id','product_image_small', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type'],
+ [['pos_categ_id','!=', false]],
+ {pricelist: shops[0].pricelist_id[0]} // context for price
+ );
+ }).pipe( function(product_list){
+ self.set({'product_list': product_list});
+ });
+
+ var bank_def = fetch(
+ 'account.bank.statement',
+ ['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
+ [['state','=','open'],['pos_session_id', '=', pos_session.id]]
+ ).then(function(result){
+ self.set({'bank_statements':result});
+ });
+
+ var journal_def = fetch(
+ 'account.journal',
+ undefined,
+ [['user_id','=',pos_session.user_id[0]]]
+ ).then(function(result){
+ self.set({'journals':result});
+ });
+
+ // associate the bank statements with their journals.
+ var bank_process_def = $.when(bank_def, journal_def)
+ .then(function(){
+ var bank_statements = self.get('bank_statements');
+ var journals = self.get('journals');
+ 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;
+ }
+ }
+ }
+ });
+
+ session_data_def = $.when(pos_config_def,bank_def,journal_def,bank_process_def);
+
+ }else{
+ session_data_def.reject();
+ }
+ return session_data_def;
+ });
+
+ // associate the products with their categories
+ var prod_process_def = $.when(cat_def, session_def)
+ .pipe(function(){
+ var product_list = self.get('product_list');
+ var categories = self.get('categories');
+ var cat_by_id = {};
+ for(var i = 0; i < categories.length; i++){
+ cat_by_id[categories[i].id] = categories[i];
+ }
+ //set the parent in the category
+ for(var i = 0; i < categories.length; i++){
+ categories[i].parent_category = cat_by_id[categories[i].parent_id[0]];
+ }
+ for(var i = 0; i < product_list.length; i++){
+ product_list[i].pos_category = cat_by_id[product_list[i].pos_categ_id[0]];
+ }
+ });
+
+ // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used.
+ $.when(pack_def, cat_def, user_def, users_def, uom_def, session_def, tax_def, prod_process_def, user_def, this.flush())
+ .then(function(){
+ self.build_categories();
+ self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))});
+ //self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging
+ self.ready.resolve();
+ },function(){
+ //we failed to load some backend data, or the backend was badly configured.
+ //the error messages will be displayed in PosWidget
+ self.ready.reject();
+ });
+ },
+
+ // logs the usefull posmodel data to the console for debug purposes
+ log_loaded_data: function(){
+ console.log('PosModel data has been loaded:');
+ console.log('PosModel: categories:',this.get('categories'));
+ console.log('PosModel: product_list:',this.get('product_list'));
+ console.log('PosModel: units:',this.get('units'));
+ console.log('PosModel: bank_statements:',this.get('bank_statements'));
+ console.log('PosModel: journals:',this.get('journals'));
+ console.log('PosModel: taxes:',this.get('taxes'));
+ console.log('PosModel: pos_session:',this.get('pos_session'));
+ console.log('PosModel: pos_config:',this.get('pos_config'));
+ console.log('PosModel: cashRegisters:',this.get('cashRegisters'));
+ console.log('PosModel: shop:',this.get('shop'));
+ console.log('PosModel: company:',this.get('company'));
+ console.log('PosModel: currency:',this.get('currency'));
+ console.log('PosModel: user_list:',this.get('user_list'));
+ console.log('PosModel: user:',this.get('user'));
+ console.log('PosModel.session:',this.session);
+ console.log('PosModel.categories:',this.categories);
+ console.log('PosModel end of data log.');
+ },
+
+ // this is called when an order is removed from the order collection. It ensures that there is always an existing
+ // order and a valid selected order
+ on_removed_order: function(removed_order){
+ if( this.get('orders').isEmpty()){
+ this.add_and_select_order(new module.Order({ pos: this }));
+ }
+ if( this.get('selectedOrder') === removed_order){
+ this.set({ selectedOrder: this.get('orders').last() });
+ }
+ },
+
+ // 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) {
+ var self = this;
+ return this.dao.add_operation(record).pipe(function(){
+ return self.flush();
+ });
+ },
+
+ add_and_select_order: function(newOrder) {
+ (this.get('orders')).add(newOrder);
+ return this.set({
+ selectedOrder: newOrder
+ });
+ },
+
+ // attemps to send all pending orders ( stored in the DAO ) to the server.
+ // it will do it one by one, and remove the successfully sent ones from the DAO once
+ // it has been confirmed that they have been received.
+ flush: function() {
+ //this makes sure only one _int_flush is called at the same time
+ return this.flush_mutex.exec(_.bind(function() {
+ return this._int_flush();
+ }, this));
+ },
+ _int_flush : function() {
+ var self = this;
+
+ this.dao.get_operations().pipe(function(operations) {
+ // operations are really Orders that are converted to json.
+ // they are saved to disk and then we attempt to send them to the backend so that they can
+ // be applied.
+ // since the network is not reliable we potentially have many 'pending operations' that have not been sent.
+ self.set( {'nbr_pending_operations':operations.length} );
+ if(operations.length === 0){
+ return $.when();
+ }
+ var order = operations[0];
+
+ // we prevent the default error handler and assume errors
+ // are a normal use case, except we stop the current iteration
+
+ return (new instance.web.Model('pos.order')).get_func('create_from_ui')([order])
+ .fail(function(unused, event){
+ // wtf ask niv
+ event.preventDefault();
+ })
+ .pipe(function(){
+ // success: remove the successfully sent operation, and try to send the next one
+ self.dao.remove_operation(operations[0].id).pipe(function(){
+ return self._int_flush();
+ });
+ }, function(){
+ // in case of error we just sit there and do nothing. wtf ask niv
+ return $.when();
+ });
+ });
+ },
+
+ // this adds several properties to the categories in order to make it easier to diplay them
+ // fields added include the list of product relevant to each category, list of child categories,
+ // list of ancestors, etc.
+ build_categories : function(){
+ var categories = this.get('categories');
+ var products = this.get('product_list');
+
+ //append the content of array2 into array1
+ function append(array1, array2){
+ for(var i = 0, len = array2.length; i < len; i++){
+ array1.push(array2[i]);
+ }
+ }
+
+ function appendSet(set1, set2){
+ for(key in set2){
+ set1[key] = set2[key];
+ }
+ }
+
+ var categories_by_id = {};
+ for(var i = 0; i < categories.length; i++){
+ categories_by_id[categories[i].id] = categories[i];
+ }
+ this.categories_by_id = categories_by_id;
+
+ var root_category = {
+ name : 'Root',
+ id : 0,
+ parent : null,
+ childrens : [],
+ };
+
+ // add parent and childrens field to categories, find root_categories
+ for(var i = 0; i < categories.length; i++){
+ var cat = categories[i];
+
+ cat.parent = categories_by_id[cat.parent_id[0]];
+ if(!cat.parent){
+ root_category.childrens.push(cat);
+ cat.parent = root_category;
+ }
+
+ cat.childrens = [];
+ for(var j = 0; j < cat.child_id.length; j++){
+ cat.childrens.push(categories_by_id[ cat.child_id[j] ]);
+ }
+ }
+
+ categories.push(root_category);
+
+ // set some default fields for next steps
+ for(var i = 0; i < categories.length; i++){
+ var cat = categories[i];
+
+ cat.product_list = []; //list of all products in the category
+ cat.product_set = {}; // [product.id] === true if product is in category
+ cat.weightable_product_list = [];
+ cat.weightable_product_set = {};
+ cat.weightable = false; //true if directly contains weightable products
+ }
+
+ this.root_category = root_category;
+
+ //we add the products to the categories.
+ for(var i = 0, len = products.length; i < len; i++){
+ var product = products[i];
+ var cat = categories_by_id[product.pos_categ_id[0]];
+ if(cat){
+ cat.product_list.push(product);
+ cat.product_set[product.id] = true;
+ if(product.to_weight){
+ cat.weightable_product_list.push(product);
+ cat.weightable_product_set[product.id] = true;
+ cat.weightable = true;
+ }
+ }
+ }
+
+ // we build a flat list of all categories that directly contains weightable products
+ this.weightable_categories = [];
+ for(var i = 0, len = categories.length; i < len; i++){
+ var cat = categories[i];
+ if(cat.weightable){
+ this.weightable_categories.push(cat);
+ }
+ }
+
+ // add ancestor field to categories, contains the list of parents of parents, from root to parent
+ function make_ancestors(cat, ancestors){
+ cat.ancestors = ancestors.slice(0);
+ ancestors.push(cat);
+
+ for(var i = 0; i < cat.childrens.length; i++){
+ make_ancestors(cat.childrens[i], ancestors.slice(0));
+ }
+ }
+
+ //add the products of the subcategories to the parent categories
+ function make_products(cat){
+ for(var i = 0; i < cat.childrens.length; i++){
+ make_products(cat.childrens[i]);
+
+ append(cat.product_list, cat.childrens[i].product_list);
+ append(cat.weightable_product_list, cat.childrens[i].weightable_product_list);
+
+ appendSet(cat.product_set, cat.childrens[i].product_set);
+ appendSet(cat.weightable_product_set, cat.childrens[i].weightable_product_set);
+ }
+ }
+
+ make_ancestors(root_category,[]);
+ make_products(root_category);
+ },
+ });
+
+ module.CashRegister = Backbone.Model.extend({
+ });
+
+ module.CashRegisterCollection = Backbone.Collection.extend({
+ model: module.CashRegister,
+ });
+
+ module.Product = Backbone.Model.extend({
+ });
+
+ module.ProductCollection = Backbone.Collection.extend({
+ model: module.Product,
+ });
+
+ // An orderline represent one element of the content of a client's shopping cart.
+ // An orderline contains a product, its quantity, its price, discount. etc.
+ // An Order contains zero or more Orderlines.
+ module.Orderline = Backbone.Model.extend({
+ initialize: function(attr,options){
+ this.pos = options.pos;
+ this.order = options.order;
+ this.product = options.product;
+ this.price = options.product.get('list_price');
+ this.quantity = 1;
+ this.discount = 0;
+ this.type = 'unit';
+ this.selected = false;
+ },
+ // sets a discount [0,100]%
+ set_discount: function(discount){
+ this.discount = Math.max(0,Math.min(100,discount));
+ this.trigger('change');
+ },
+ // returns the discount [0,100]%
+ get_discount: function(){
+ return this.discount;
+ },
+ // FIXME
+ get_product_type: function(){
+ return this.type;
+ },
+ // sets the quantity of the product. The quantity will be rounded according to the
+ // product's unity of measure properties. Quantities greater than zero will not get
+ // rounded to zero
+ set_quantity: function(quantity){
+ if(_.isNaN(quantity)){
+ this.order.removeOrderline(this);
+ }else if(quantity !== undefined){
+ this.quantity = Math.max(0,quantity);
+ var unit = this.get_unit();
+ if(unit && this.quantity > 0 ){
+ this.quantity = Math.max(unit.rounding, Math.round(quantity / unit.rounding) * unit.rounding);
+ }
+ }
+ this.trigger('change');
+ },
+ // return the quantity of product
+ get_quantity: function(){
+ return this.quantity;
+ },
+ // return the unit of measure of the product
+ get_unit: function(){
+ var unit_id = (this.product.get('uos_id') || this.product.get('uom_id'));
+ if(!unit_id){
+ return undefined;
+ }
+ unit_id = unit_id[0];
+ if(!this.pos){
+ return undefined;
+ }
+ return this.pos.get('units_by_id')[unit_id];
+ },
+ // return the product of this orderline
+ get_product: function(){
+ return this.product;
+ },
+ // return the base price of this product (for this orderline)
+ get_list_price: function(){
+ return this.price;
+ },
+ // changes the base price of the product for this orderline
+ set_list_price: function(price){
+ this.price = price;
+ this.trigger('change');
+ },
+ // selects or deselects this orderline
+ set_selected: function(selected){
+ this.selected = selected;
+ this.trigger('change');
+ },
+ // returns true if this orderline is selected
+ is_selected: function(){
+ return this.selected;
+ },
+ // when we add an new orderline we want to merge it with the last line to see reduce the number of items
+ // in the orderline. This returns true if it makes sense to merge the two
+ can_be_merged_with: function(orderline){
+ if( this.get_product().get('id') !== orderline.get_product().get('id')){ //only orderline of the same product can be merged
+ return false;
+ }else if(this.get_product_type() !== orderline.get_product_type()){
+ return false;
+ }else if(this.get_discount() > 0){ // we don't merge discounted orderlines
+ return false;
+ }else if(this.get_product_type() === 'unit'){
+ return true;
+ }else if(this.get_product_type() === 'weight'){
+ return true;
+ }else if(this.get_product_type() === 'price'){
+ return this.get_product().get('list_price') === orderline.get_product().get('list_price');
+ }else{
+ console.error('point_of_sale/pos_models.js/Orderline.can_be_merged_with() : unknown product type:',this.get('product_type'));
+ return false;
+ }
+ },
+ merge: function(orderline){
+ this.set_quantity(this.get_quantity() + orderline.get_quantity());
+ },
+ export_as_JSON: function() {
+ return {
+ qty: this.get_quantity(),
+ price_unit: this.get_list_price(),
+ discount: this.get_discount(),
+ product_id: this.get_product().get('id'),
+ };
+ },
+ //used to create a json of the ticket, to be sent to the printer
+ export_for_printing: function(){
+ return {
+ quantity: this.get_quantity(),
+ unit_name: this.get_unit().name,
+ list_price: this.get_list_price(),
+ discount: this.get_discount(),
+ product_name: this.get_product().get('name'),
+ price_with_tax : this.get_price_with_tax(),
+ price_without_tax: this.get_price_without_tax(),
+ tax: this.get_tax(),
+ };
+ },
+ get_price_without_tax: function(){
+ return this.get_all_prices().priceWithoutTax;
+ },
+ get_price_with_tax: function(){
+ return this.get_all_prices().priceWithTax;
+ },
+ get_tax: function(){
+ return this.get_all_prices().tax;
+ },
+ get_all_prices: function() {
+ var self = this;
+ var base = this.get_quantity() * this.price * (1 - (this.get_discount() / 100));
+ var totalTax = base;
+ var totalNoTax = base;
+
+ var product_list = this.pos.get('product_list');
+ var product = this.get_product();
+ var taxes_ids = product.taxes_id;
+ var taxes = self.pos.get('taxes');
+ var taxtotal = 0;
+ _.each(taxes_ids, function(el) {
+ var tax = _.detect(taxes, function(t) {return t.id === el;});
+ if (tax.price_include) {
+ var tmp;
+ if (tax.type === "percent") {
+ tmp = base - (base / (1 + tax.amount));
+ } else if (tax.type === "fixed") {
+ tmp = tax.amount * self.get_quantity();
+ } else {
+ throw "This type of tax is not supported by the point of sale: " + tax.type;
+ }
+ taxtotal += tmp;
+ totalNoTax -= tmp;
+ } else {
+ var tmp;
+ if (tax.type === "percent") {
+ tmp = tax.amount * base;
+ } else if (tax.type === "fixed") {
+ tmp = tax.amount * self.get_quantity();
+ } else {
+ throw "This type of tax is not supported by the point of sale: " + tax.type;
+ }
+ taxtotal += tmp;
+ totalTax += tmp;
+ }
+ });
+ return {
+ "priceWithTax": totalTax,
+ "priceWithoutTax": totalNoTax,
+ "tax": taxtotal,
+ };
+ },
+ });
+
+ module.OrderlineCollection = Backbone.Collection.extend({
+ model: module.Orderline,
+ });
+
+ // Every PaymentLine contains a cashregister and an amount of money.
+ module.Paymentline = Backbone.Model.extend({
+ initialize: function(attributes, options) {
+ this.amount = 0;
+ this.cashregister = options.cashRegister;
+ },
+ //sets the amount of money on this payment line
+ set_amount: function(value){
+ this.amount = value;
+ this.trigger('change');
+ },
+ // returns the amount of money on this paymentline
+ get_amount: function(){
+ return this.amount;
+ },
+ // returns the associated cashRegister
+ get_cashregister: function(){
+ return this.cashregister;
+ },
+ //exports as JSON for server communication
+ export_as_JSON: function(){
+ return {
+ name: instance.web.datetime_to_str(new Date()),
+ statement_id: this.cashregister.get('id'),
+ account_id: (this.cashregister.get('account_id'))[0],
+ journal_id: (this.cashregister.get('journal_id'))[0],
+ amount: this.get_amount()
+ };
+ },
+ //exports as JSON for receipt printing
+ export_for_printing: function(){
+ return {
+ amount: this.get_amount(),
+ journal: this.cashregister.get('journal_id')[1],
+ };
+ },
+ });
+
+ module.PaymentlineCollection = Backbone.Collection.extend({
+ model: module.Paymentline,
+ });
+
+
+ // An order more or less represents the content of a client's shopping cart (the OrderLines)
+ // plus the associated payment information (the PaymentLines)
+ // there is always an active ('selected') order in the Pos, a new one is created
+ // automaticaly once an order is completed and sent to the server.
+ module.Order = Backbone.Model.extend({
+ initialize: function(attributes){
+ Backbone.Model.prototype.initialize.apply(this, arguments);
+ this.set({
+ creationDate: new Date(),
+ orderLines: new module.OrderlineCollection(),
+ paymentLines: new module.PaymentlineCollection(),
+ name: "Order " + this.generateUniqueId(),
+ client: null,
+ });
+ this.pos = attributes.pos;
+ this.selected_orderline = undefined;
+ this.screen_data = {}; // see ScreenSelector
+ return this;
+ },
+ generateUniqueId: function() {
+ return new Date().getTime();
+ },
+ addProduct: function(product, options){
+ options = options || {};
+ var attr = product.toJSON();
+ attr.pos = this.pos;
+ attr.order = this;
+ var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
+
+ if(options.quantity !== undefined){
+ line.set_quantity(options.quantity);
+ }
+ if(options.price !== undefined){
+ line.set_list_price(options.price);
+ }
+
+ var last_orderline = this.getLastOrderline();
+ if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){
+ last_orderline.merge(line);
+ }else{
+ this.get('orderLines').add(line);
+ }
+ this.selectLine(this.getLastOrderline());
+ },
+ removeOrderline: function( line ){
+ this.get('orderLines').remove(line);
+ this.selectLine(this.getLastOrderline());
+ },
+ getLastOrderline: function(){
+ return this.get('orderLines').at(this.get('orderLines').length -1);
+ },
+ addPaymentLine: function(cashRegister) {
+ var paymentLines = this.get('paymentLines');
+ var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
+ if(cashRegister.get('journal').type !== 'cash'){
+ newPaymentline.set_amount( this.getDueLeft() );
+ }
+ paymentLines.add(newPaymentline);
+ },
+ getName: function() {
+ return this.get('name');
+ },
+ getTotal: function() {
+ return (this.get('orderLines')).reduce((function(sum, orderLine) {
+ return sum + orderLine.get_price_with_tax();
+ }), 0);
+ },
+ getTotalTaxExcluded: function() {
+ return (this.get('orderLines')).reduce((function(sum, orderLine) {
+ return sum + orderLine.get_price_without_tax();
+ }), 0);
+ },
+ getTax: function() {
+ return (this.get('orderLines')).reduce((function(sum, orderLine) {
+ return sum + orderLine.get_tax();
+ }), 0);
+ },
+ getPaidTotal: function() {
+ return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
+ return sum + paymentLine.get_amount();
+ }), 0);
+ },
+ getChange: function() {
+ return this.getPaidTotal() - this.getTotal();
+ },
+ getDueLeft: function() {
+ return this.getTotal() - this.getPaidTotal();
+ },
+ // the client related to the current order.
+ set_client: function(client){
+ this.set('client',client);
+ },
+ get_client: function(){
+ return this.get('client');
+ },
+ // the order also stores the screen status, as the PoS supports
+ // different active screens per order. This method is used to
+ // store the screen status.
+ set_screen_data: function(key,value){
+ if(arguments.length === 2){
+ this.screen_data[key] = value;
+ }else if(arguments.length === 1){
+ for(key in arguments[0]){
+ this.screen_data[key] = arguments[0][key];
+ }
+ }
+ },
+ //see set_screen_data
+ get_screen_data: function(key){
+ return this.screen_data[key];
+ },
+ // exports a JSON for receipt printing
+ export_for_printing: function(){
+ var orderlines = [];
+ this.get('orderLines').each(function(orderline){
+ orderlines.push(orderline.export_for_printing());
+ });
+
+ var paymentlines = [];
+ this.get('paymentLines').each(function(paymentline){
+ paymentlines.push(paymentline.export_for_printing());
+ });
+ var client = this.get('client');
+ var cashier = this.pos.get('cashier') || this.pos.get('user');
+ var company = this.pos.get('company');
+ var shop = this.pos.get('shop');
+ var date = new Date();
+
+ return {
+ orderlines: orderlines,
+ paymentlines: paymentlines,
+ total_with_tax: this.getTotal(),
+ total_without_tax: this.getTotalTaxExcluded(),
+ total_tax: this.getTax(),
+ total_paid: this.getPaidTotal(),
+ change: this.getChange(),
+ name : this.getName(),
+ client: client ? client.name : null ,
+ cashier: cashier ? cashier.name : null,
+ date: {
+ year: date.getFullYear(),
+ month: date.getMonth(),
+ date: date.getDate(), // day of the month
+ day: date.getDay(), // day of the week
+ hour: date.getHours(),
+ minute: date.getMinutes()
+ },
+ company:{
+ email: company.email,
+ website: company.website,
+ company_registry: company.company_registry,
+ contact_address: null, //TODO
+ vat: company.vat,
+ name: company.name,
+ phone: company.phone,
+ },
+ shop:{
+ name: shop.name,
+ },
+ currency: this.pos.get('currency'),
+ };
+ },
+ exportAsJSON: function() {
+ var orderLines, paymentLines;
+ orderLines = [];
+ (this.get('orderLines')).each(_.bind( function(item) {
+ return orderLines.push([0, 0, item.export_as_JSON()]);
+ }, this));
+ paymentLines = [];
+ (this.get('paymentLines')).each(_.bind( function(item) {
+ return paymentLines.push([0, 0, item.export_as_JSON()]);
+ }, this));
+ return {
+ name: this.getName(),
+ amount_paid: this.getPaidTotal(),
+ amount_total: this.getTotal(),
+ amount_tax: this.getTax(),
+ amount_return: this.getChange(),
+ lines: orderLines,
+ statement_ids: paymentLines,
+ pos_session_id: this.pos.get('pos_session').id,
+ partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
+ user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
+ };
+ },
+ getSelectedLine: function(){
+ return this.selected_orderline;
+ },
+ selectLine: function(line){
+ if(line){
+ if(line !== this.selected_orderline){
+ if(this.selected_orderline){
+ this.selected_orderline.set_selected(false);
+ }
+ this.selected_orderline = line;
+ this.selected_orderline.set_selected(true);
+ }
+ }else{
+ this.selected_orderline = undefined;
+ }
+ },
+ });
+
+ module.OrderCollection = Backbone.Collection.extend({
+ model: module.Order,
+ });
+
+ /*
+ The numpad handles both the choice of the property currently being modified
+ (quantity, price or discount) and the edition of the corresponding numeric value.
+ */
+ module.NumpadState = Backbone.Model.extend({
+ defaults: {
+ buffer: "0",
+ mode: "quantity"
+ },
+ appendNewChar: function(newChar) {
+ var oldBuffer;
+ oldBuffer = this.get('buffer');
+ if (oldBuffer === '0') {
+ this.set({
+ buffer: newChar
+ });
+ } else if (oldBuffer === '-0') {
+ this.set({
+ buffer: "-" + newChar
+ });
+ } else {
+ this.set({
+ buffer: (this.get('buffer')) + newChar
+ });
+ }
+ this.updateTarget();
+ },
+ deleteLastChar: function() {
+ var tempNewBuffer = this.get('buffer').slice(0, -1);
+
+ if(!tempNewBuffer){
+ this.set({ buffer: "0" });
+ this.killTarget();
+ }else{
+ if (isNaN(tempNewBuffer)) {
+ tempNewBuffer = "0";
+ }
+ this.set({ buffer: tempNewBuffer });
+ this.updateTarget();
+ }
+ },
+ switchSign: function() {
+ var oldBuffer;
+ oldBuffer = this.get('buffer');
+ this.set({
+ buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
+ });
+ this.updateTarget();
+ },
+ changeMode: function(newMode) {
+ this.set({
+ buffer: "0",
+ mode: newMode
+ });
+ },
+ reset: function() {
+ this.set({
+ buffer: "0",
+ mode: "quantity"
+ });
+ },
+ updateTarget: function() {
+ var bufferContent, params;
+ bufferContent = this.get('buffer');
+ if (bufferContent && !isNaN(bufferContent)) {
+ this.trigger('set_value', parseFloat(bufferContent));
+ }
+ },
+ killTarget: function(){
+ this.trigger('set_value',Number.NaN);
+ },
+ });
+}
diff --git a/addons/point_of_sale/static/src/js/pos_screens.js b/addons/point_of_sale/static/src/js/pos_screens.js
new file mode 100644
index 00000000000..eeb45e8a27d
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_screens.js
@@ -0,0 +1,772 @@
+
+// this file contains the screens definitions. Screens are the
+// content of the right pane of the pos, containing the main functionalities.
+// screens are contained in the PosWidget, in pos_widget.js
+// all screens are present in the dom at all time, but only one is shown at the
+// same time.
+//
+// transition between screens is made possible by the use of the screen_selector,
+// which is responsible of hiding and showing the screens, as well as maintaining
+// the state of the screens between different orders.
+//
+// all screens inherit from ScreenWidget. the only addition from the base widgets
+// are show() and hide() which shows and hides the screen but are also used to
+// bind and unbind actions on widgets and devices. The screen_selector guarantees
+// that only one screen is shown at the same time and that show() is called after all
+// hide()s
+
+function openerp_pos_screens(instance, module){ //module is instance.point_of_sale
+ var QWeb = instance.web.qweb;
+
+ module.ScreenSelector = instance.web.Class.extend({
+ init: function(options){
+ this.pos = options.pos;
+
+ this.screen_set = options.screen_set || {};
+
+ this.popup_set = options.popup_set || {};
+
+ this.default_client_screen = options.default_client_screen;
+ this.default_cashier_screen = options.default_cashier_screen;
+
+ this.current_popup = null;
+
+ this.current_mode = options.default_mode || 'client';
+
+ this.current_screen = null;
+
+ for(screen_name in this.screen_set){
+ this.screen_set[screen_name].hide();
+ }
+
+ for(popup_name in this.popup_set){
+ this.popup_set[popup_name].hide();
+ }
+
+ this.selected_order = this.pos.get('selectedOrder');
+ this.selected_order.set_screen_data({
+ client_screen: this.default_client_screen,
+ cashier_screen: this.default_cashier_screen,
+ });
+
+ this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
+ },
+ add_screen: function(screen_name, screen){
+ screen.hide();
+ this.screen_set[screen_name] = screen;
+ return this;
+ },
+ show_popup: function(name){
+ if(this.current_popup){
+ this.close_popup();
+ }
+ this.current_popup = this.popup_set[name];
+ this.current_popup.show();
+ },
+ close_popup: function(){
+ if(this.current_popup){
+ this.current_popup.hide();
+ this.current_popup = null;
+ }
+ },
+ load_saved_screen: function(){
+ this.close_popup();
+
+ var selectedOrder = this.pos.get('selectedOrder');
+
+ if(this.current_mode === 'client'){
+ this.set_current_screen(selectedOrder.get_screen_data('client_screen') || this.default_client_screen,null,'refresh');
+ }else if(this.current_mode === 'cashier'){
+ this.set_current_screen(selectedOrder.get_screen_data('cashier_screen') || this.default_cashier_screen,null,'refresh');
+ }
+ this.selected_order = selectedOrder;
+ },
+ set_user_mode: function(user_mode){
+ if(user_mode !== this.current_mode){
+ this.close_popup();
+ this.current_mode = user_mode;
+ this.load_saved_screen();
+ }
+ },
+ get_user_mode: function(){
+ return this.current_mode;
+ },
+ set_current_screen: function(screen_name,params,refresh){
+ var screen = this.screen_set[screen_name];
+ if(!screen){
+ console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
+ }
+
+ this.close_popup();
+ var selectedOrder = this.pos.get('selectedOrder');
+ if(this.current_mode === 'client'){
+ selectedOrder.set_screen_data('client_screen',screen_name);
+ if(params){
+ selectedOrder.set_screen_data('client_screen_params',params);
+ }
+ }else{
+ selectedOrder.set_screen_data('cashier_screen',screen_name);
+ if(params){
+ selectedOrder.set_screen_data('cashier_screen_params',params);
+ }
+ }
+
+ if(screen && (refresh || screen !== this.current_screen)){
+ if(this.current_screen){
+ this.current_screen.close();
+ this.current_screen.hide();
+ }
+ this.current_screen = screen;
+ this.current_screen.show();
+ }
+ },
+ get_current_screen_param: function(param){
+ var selected_order = this.pos.get('selectedOrder');
+ if(this.current_mode === 'client'){
+ var params = selected_order.get_screen_data('client_screen_params');
+ }else{
+ var params = selected_order.get_screen_data('cashier_screen_params');
+ }
+ if(params){
+ return params[param];
+ }else{
+ return undefined;
+ }
+ },
+ set_default_screen: function(){
+ this.set_current_screen(this.current_mode === 'client' ? this.default_client_screen : this.default_cashier_screen);
+ },
+ });
+
+ module.ScreenWidget = module.PosBaseWidget.extend({
+
+ show_numpad: true,
+ show_leftpane: true,
+
+ init: function(parent,options){
+ this._super(parent,options);
+ this.hidden = false;
+ },
+
+ help_button_action: function(){
+ this.pos_widget.screen_selector.show_popup('help');
+ },
+
+ logout_button_action: function(){
+ this.pos_widget.screen_selector.set_user_mode('client');
+ },
+
+ close_button_action: function(){
+ this.pos_widget.try_close();
+ },
+
+ barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
+ barcode_product_error_popup: 'error', //if defined, this popup will be loaded when there's an error in the popup
+
+ // what happens when a product is scanned :
+ // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
+ // there's an error.
+ barcode_product_action: function(ean){
+ if(this.pos_widget.scan_product(ean)){
+ this.pos.proxy.scan_item_success();
+ if(this.barcode_product_screen){
+ this.pos_widget.screen_selector.set_current_screen(this.barcode_product_screen);
+ }
+ }else{
+ if(this.barcode_product_error_popup){
+ this.pos_widget.screen_selector.show_popup(this.barcode_product_error_popup);
+ }
+ }
+ },
+
+ // what happens when a cashier id barcode is scanned.
+ // the default behavior is the following :
+ // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
+ // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
+ barcode_cashier_action: function(ean){
+ var users = this.pos.get('user_list');
+ for(var i = 0, len = users.length; i < len; i++){
+ if(users[i].ean13 === ean.ean){
+ this.pos.set('cashier',users[i]);
+ this.pos_widget.username.refresh();
+ this.pos.proxy.cashier_mode_activated();
+ this.pos_widget.screen_selector.set_user_mode('cashier');
+ return true;
+ }
+ }
+ return false;
+ },
+
+ // what happens when a client id barcode is scanned.
+ // the default behavior is the following :
+ // - if there's a user with a matching ean, put it as the active 'client' and return true
+ // - else : return false.
+ barcode_client_action: function(ean){
+ var users = this.pos.get('user_list');
+ for(var i = 0, len = users.length; i < len; i++){
+ if(users[i].ean13 === ean.ean){
+ this.pos.get('selectedOrder').set_client(users[i]);
+ this.pos_widget.username.refresh();
+ return true;
+ }
+ }
+ return false;
+ //TODO start the transaction
+ },
+
+ // what happens when a discount barcode is scanned : the default behavior
+ // is to set the discount on the last order.
+ barcode_discount_action: function(ean){
+ var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
+ if(last_orderline){
+ last_orderline.set_discount(ean.value)
+ }
+ },
+
+ // this method shows the screen and sets up all the widget related to this screen. Extend this method
+ // if you want to alter the behavior of the screen.
+ show: function(){
+ this.hidden = false;
+ if(this.$element){
+ this.$element.show();
+ }
+
+ var self = this;
+ var cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
+
+ this.pos_widget.set_numpad_visible(this.show_numpad && cashier_mode);
+ this.pos_widget.set_leftpane_visible(this.show_leftpane);
+ this.pos_widget.set_cashier_controls_visible(cashier_mode);
+ this.pos_widget.action_bar.set_element_visible('help-button', !cashier_mode, function(){ self.help_button_action(); });
+ this.pos_widget.action_bar.set_element_visible('logout-button', cashier_mode && this.pos.use_selfcheckout, function(){ self.logout_button_action(); });
+ this.pos_widget.action_bar.set_element_visible('close-button', cashier_mode, function(){ self.close_button_action(); });
+
+ this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
+
+ this.pos.barcode_reader.set_action_callback({
+ 'cashier': self.barcode_cashier_action ? function(ean){ self.barcode_cashier_action(ean); } : undefined ,
+ 'product': self.barcode_product_action ? function(ean){ self.barcode_product_action(ean); } : undefined ,
+ 'client' : self.barcode_client_action ? function(ean){ self.barcode_client_action(ean); } : undefined ,
+ 'discount': self.barcode_discount_action ? function(ean){ self.barcode_discount_action(ean); } : undefined,
+ });
+ },
+
+
+ // this method is called when the screen is closed to make place for a new screen. this is a good place
+ // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
+ close: function(){
+ if(this.pos.barcode_reader){
+ this.pos.barcode_reader.reset_action_callbacks();
+ }
+ if(this.pos_widget.action_bar){
+ this.pos_widget.action_bar.destroy_buttons();
+ }
+ },
+
+ // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
+ // POS initialization.
+ hide: function(){
+ this.hidden = true;
+ if(this.$element){
+ this.$element.hide();
+ }
+ },
+
+ // we need this because some screens re-render themselves when they are hidden
+ // (due to some events, or magic, or both...) we must make sure they remain hidden.
+ // the good solution would probably be to make them not re-render themselves when they
+ // are hidden.
+ renderElement: function(){
+ this._super();
+ if(this.hidden){
+ if(this.$element){
+ this.$element.hide();
+ }
+ }
+ },
+ });
+
+ module.PopUpWidget = module.PosBaseWidget.extend({
+ show: function(){
+ if(this.$element){
+ this.$element.show();
+ }
+ },
+ hide: function(){
+ if(this.$element){
+ this.$element.hide();
+ }
+ },
+ });
+
+ module.HelpPopupWidget = module.PopUpWidget.extend({
+ template:'HelpPopupWidget',
+ show: function(){
+ this._super();
+ this.pos.proxy.help_needed();
+ var self = this;
+
+ this.$element.find('.button').off('click').click(function(){
+ self.pos_widget.screen_selector.close_popup();
+ self.pos.proxy.help_canceled();
+ });
+ },
+ });
+
+ module.ErrorPopupWidget = module.PopUpWidget.extend({
+ template:'ErrorPopupWidget',
+ show: function(){
+ var self = this;
+ this._super();
+ this.pos.proxy.help_needed();
+ this.pos.proxy.scan_item_error_unrecognized();
+
+ this.pos.barcode_reader.save_callbacks();
+ this.pos.barcode_reader.reset_action_callbacks();
+ this.pos.barcode_reader.set_action_callback({
+ 'cashier': function(ean){
+ clearInterval(this.intervalID);
+ self.pos.proxy.cashier_mode_activated();
+ self.pos_widget.screen_selector.set_user_mode('cashier');
+ },
+ });
+ },
+ close:function(){
+ this._super();
+ this.pos.proxy.help_canceled();
+ this.pos.barcode_reader.restore_callbacks();
+ },
+ });
+
+ module.ErrorProductNotRecognizedPopupWidget = module.ErrorPopupWidget.extend({
+ template:'ErrorProductNotRecognizedPopupWidget',
+ });
+
+ module.ErrorNoSessionPopupWidget = module.ErrorPopupWidget.extend({
+ template:'ErrorNoSessionPopupWidget',
+ });
+
+ module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
+ template:'ScaleInviteScreenWidget',
+
+ show: function(){
+ this._super();
+ var self = this;
+
+ self.pos.proxy.weighting_start();
+
+ this.intervalID = setInterval(function(){
+ var weight = self.pos.proxy.weighting_read_kg();
+ if(weight > 0.001){
+ clearInterval(this.intervalID);
+ self.pos_widget.screen_selector.set_current_screen('scale');
+ }
+ },500);
+
+ this.pos_widget.action_bar.add_new_button(
+ {
+ label: 'back',
+ icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
+ click: function(){
+ clearInterval(this.intervalID);
+ self.pos.proxy.weighting_end();
+ self.pos_widget.screen_selector.set_current_screen('products');
+ }
+ }
+ );
+ },
+ close: function(){
+ this._super();
+ clearInterval(this.intervalID);
+ },
+ });
+
+ module.ScaleScreenWidget = module.ScreenWidget.extend({
+ template:'ScaleScreenWidget',
+ show: function(){
+ this._super();
+ this.renderElement();
+ var self = this;
+
+ this.pos_widget.action_bar.add_new_button({
+ label: 'back',
+ icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
+ click: function(){
+ self.pos_widget.screen_selector.set_current_screen('products');
+ }
+ });
+
+ this.validate_button = this.pos_widget.action_bar.add_new_button({
+ label: 'Validate',
+ icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
+ click: function(){
+ self.order_product();
+ self.pos_widget.screen_selector.set_current_screen('products');
+ },
+ });
+
+ this.pos.proxy.weighting_start();
+ this.intervalID = setInterval(function(){
+ var weight = self.pos.proxy.weighting_read_kg();
+ if(weight != self.weight){
+ self.weight = weight;
+ self.renderElement();
+ }
+ },200);
+ },
+ renderElement: function(){
+ var self = this;
+ this._super();
+ this.$('.product-picture').click(function(){
+ self.order_product();
+ self.pos_widget.screen_selector.set_current_screen('products');
+ });
+ },
+ get_product: function(){
+ var ss = this.pos_widget.screen_selector;
+ if(ss){
+ return ss.get_current_screen_param('product');
+ }else{
+ return undefined;
+ }
+ },
+ order_product: function(){
+ var weight = this.pos.proxy.weighting_read_kg();
+ this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity:weight });
+ },
+ get_product_name: function(){
+ var product = this.get_product();
+ return (product ? product.get('name') : undefined) || 'Unnamed Product';
+ },
+ get_product_price: function(){
+ var product = this.get_product();
+ return (product ? product.get('list_price') : 0) || 0;
+ },
+ get_product_image: function(){
+ var product = this.get_product();
+ return product ? product.get('product_image_small') : undefined;
+ },
+ get_product_weight: function(){
+ return this.weight || 0;
+ },
+ close: function(){
+ this._super();
+ clearInterval(this.intervalID);
+ this.pos.proxy.weighting_end();
+ },
+ });
+
+ module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
+ template:'ClientPaymentScreenWidget',
+ show: function(){
+ this._super();
+ var self = this;
+
+ this.pos.proxy.payment_request(this.pos.get('selectedOrder').getDueLeft(),'card','info'); //TODO TOTAL
+
+ this.intervalID = setInterval(function(){
+ var payment = self.pos.proxy.is_payment_accepted();
+ if(payment === 'payment_accepted'){
+ clearInterval(this.intervalID);
+
+ var currentOrder = self.pos.get('selectedOrder');
+
+ //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];
+ if(cashregister.self_checkout_payment_method){
+ selfCheckoutRegisters.push(cashregister);
+ }
+ }
+
+ 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('welcome');
+ });
+ }else if(payment === 'payment_rejected'){
+ clearInterval(this.intervalID);
+ //TODO show a tryagain thingie ?
+ }
+ },500);
+
+ this.pos_widget.action_bar.add_new_button(
+ {
+ label: 'back',
+ icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
+ click: function(){ //TODO Go to ask for weighting screen
+ clearInterval(this.intervalID);
+ self.pos.proxy.payment_canceled();
+ self.pos_widget.screen_selector.set_current_screen('products');
+ }
+ }
+ );
+ },
+ close: function(){
+ this._super();
+ clearInterval(this.intervalID);
+ },
+ });
+
+ module.WelcomeScreenWidget = module.ScreenWidget.extend({
+ template:'WelcomeScreenWidget',
+
+ show_numpad: false,
+ show_leftpane: false,
+
+ barcode_client_action: function(ean){
+ this._super(ean);
+ this.pos_widget.screen_selector.set_current_screen('products');
+ },
+
+ show: function(){
+ this._super();
+ var self = this;
+ },
+ });
+
+ module.ProductScreenWidget = module.ScreenWidget.extend({
+ template:'ProductScreenWidget',
+
+ show_numpad: true,
+ show_leftpane: true,
+
+ start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
+ var self = this;
+ this.product_categories_widget = new module.ProductCategoriesWidget(this,{});
+ this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
+
+ this.product_list_widget = new module.ProductListWidget(this,{
+ click_product_action: function(product){
+ if(product.get('to_weight') && self.pos.use_scale){
+ self.pos_widget.screen_selector.set_current_screen('scale_invite', {product: product});
+ }else{
+ self.pos.get('selectedOrder').addProduct(product);
+ }
+ },
+ });
+ this.product_list_widget.replace($('.placeholder-ProductListWidget'));
+ },
+
+ show: function(){
+ this._super();
+ var self = this;
+
+ this.product_categories_widget.reset_category();
+
+ this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state);
+ if(this.pos.use_virtual_keyboard){
+ this.pos_widget.onscreen_keyboard.connect();
+ }
+
+ if(this.pos_widget.screen_selector.current_mode === 'client'){
+ this.pos_widget.action_bar.add_new_button({
+ label: 'pay',
+ icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
+ click: function(){
+ self.pos_widget.screen_selector.set_current_screen('client_payment');
+ }
+ });
+ }
+ },
+
+ close: function(){
+ this._super();
+ this.pos_widget.order_widget.set_numpad_state(null);
+ this.pos_widget.payment_screen.set_numpad_state(null);
+ },
+
+ });
+
+ module.ReceiptScreenWidget = module.ScreenWidget.extend({
+ template: 'ReceiptScreenWidget',
+
+ show_numpad: true,
+ show_leftpane: true,
+
+ init: function(parent, options) {
+ this._super(parent,options);
+ this.model = options.model;
+ this.user = this.pos.get('user');
+ this.company = this.pos.get('company');
+ this.shop_obj = this.pos.get('shop');
+ },
+ renderElement: function() {
+ this._super();
+ this.pos.bind('change:selectedOrder', this.change_selected_order, this);
+ this.change_selected_order();
+ },
+ show: function(){
+ this._super();
+ var self = this;
+
+ this.pos_widget.action_bar.add_new_button({
+ label: 'Print',
+ icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
+ click: function(){ self.print(); },
+ });
+
+ this.pos_widget.action_bar.add_new_button({
+ label: 'Next Order',
+ icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
+ click: function() { self.finishOrder(); },
+ });
+ },
+ print: function() {
+ window.print();
+ },
+ finishOrder: function() {
+ this.pos.get('selectedOrder').destroy();
+ },
+ change_selected_order: function() {
+ if (this.currentOrderLines)
+ this.currentOrderLines.unbind();
+ this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
+ this.currentOrderLines.bind('add', this.refresh, this);
+ this.currentOrderLines.bind('change', this.refresh, this);
+ this.currentOrderLines.bind('remove', this.refresh, this);
+ if (this.currentPaymentLines)
+ this.currentPaymentLines.unbind();
+ this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
+ this.currentPaymentLines.bind('all', this.refresh, this);
+ this.refresh();
+ },
+ refresh: function() {
+ this.currentOrder = this.pos.get('selectedOrder');
+ $('.pos-receipt-container', this.$element).html(QWeb.render('PosTicket',{widget:this}));
+ },
+ });
+
+ module.PaymentScreenWidget = module.ScreenWidget.extend({
+ template: 'PaymentScreenWidget',
+ init: function(parent, options) {
+ this._super(parent,options);
+ this.model = options.model;
+ this.pos.bind('change:selectedOrder', this.change_selected_order, this);
+ this.bindPaymentLineEvents();
+ this.bind_orderline_events();
+ },
+ show: function(){
+ this._super();
+ var self = this;
+
+ if(this.pos.use_cashbox){
+ this.pos.proxy.open_cashbox();
+ }
+
+ this.set_numpad_state(this.pos_widget.numpad.state);
+
+ this.back_button = this.pos_widget.action_bar.add_new_button({
+ label: 'Back',
+ icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
+ click: function(){
+ self.pos_widget.screen_selector.set_current_screen('products');
+ },
+ });
+
+ this.validate_button = this.pos_widget.action_bar.add_new_button({
+ label: 'Validate',
+ icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
+ click: function(){
+ self.validateCurrentOrder();
+ },
+ });
+ },
+ close: function(){
+ this._super();
+ this.pos_widget.order_widget.set_numpad_state(null);
+ this.pos_widget.payment_screen.set_numpad_state(null);
+ },
+ back: function() {
+ this.pos_widget.screen_selector.set_current_screen('products');
+ },
+ validateCurrentOrder: function() {
+ var self = this;
+ var currentOrder = this.pos.get('selectedOrder');
+
+ this.validate_button.$element.attr('disabled','disabled'); //FIXME is the css actually using this attr ?
+
+ this.pos.push_order(currentOrder.exportAsJSON())
+ .then(function() {
+ self.validate_button.$element.removeAttr('disabled');
+ if(self.pos.use_proxy_printer){
+ self.pos.proxy.print_receipt(currentOrder.export_for_printing());
+ self.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
+ }else{
+ self.pos_widget.screen_selector.set_current_screen('receipt');
+ }
+ });
+ },
+ bindPaymentLineEvents: function() {
+ this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
+ this.currentPaymentLines.bind('add', this.addPaymentLine, this);
+ this.currentPaymentLines.bind('remove', this.renderElement, this);
+ this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
+ },
+ bind_orderline_events: function() {
+ this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
+ this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
+ },
+ change_selected_order: function() {
+ this.currentPaymentLines.unbind();
+ this.bindPaymentLineEvents();
+ this.currentOrderLines.unbind();
+ this.bind_orderline_events();
+ this.renderElement();
+ },
+ addPaymentLine: function(newPaymentLine) {
+ var x = new module.PaymentlineWidget(null, {
+ payment_line: newPaymentLine
+ });
+ x.on_delete.add(_.bind(this.deleteLine, this, x));
+ x.appendTo(this.$('#paymentlines'));
+ },
+ renderElement: function() {
+ this._super();
+ this.$('#paymentlines').empty();
+ this.currentPaymentLines.each(_.bind( function(paymentLine) {
+ this.addPaymentLine(paymentLine);
+ }, this));
+ this.updatePaymentSummary();
+ },
+ deleteLine: function(lineWidget) {
+ this.currentPaymentLines.remove([lineWidget.payment_line]);
+ },
+ updatePaymentSummary: function() {
+ var currentOrder = this.pos.get('selectedOrder');
+ var paidTotal = currentOrder.getPaidTotal();
+ var dueTotal = currentOrder.getTotal();
+ var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
+ var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
+
+ this.$('#payment-due-total').html(dueTotal.toFixed(2));
+ this.$('#payment-paid-total').html(paidTotal.toFixed(2));
+ this.$('#payment-remaining').html(remaining.toFixed(2));
+ this.$('#payment-change').html(change.toFixed(2));
+ },
+ set_numpad_state: function(numpadState) {
+ if (this.numpadState) {
+ this.numpadState.unbind('set_value', this.set_value);
+ this.numpadState.unbind('change:mode', this.setNumpadMode);
+ }
+ this.numpadState = numpadState;
+ if (this.numpadState) {
+ this.numpadState.bind('set_value', this.set_value, this);
+ this.numpadState.bind('change:mode', this.setNumpadMode, this);
+ this.numpadState.reset();
+ this.setNumpadMode();
+ }
+ },
+ setNumpadMode: function() {
+ this.numpadState.set({mode: 'payment'});
+ },
+ set_value: function(val) {
+ this.currentPaymentLines.last().set({amount: val});
+ },
+ });
+
+}
diff --git a/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js b/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js
new file mode 100644
index 00000000000..d4f6903c435
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_scrollbar_widget.js
@@ -0,0 +1,268 @@
+/*
+ * This Widget provides a javascript scrollbar that is suitable to use with resistive
+ * tactile screens.
+ *
+ * Options:
+ * target_widget : the widget that will be scrolled.
+ * target_selector : if you don't want to scroll the root element of the the widget, you can provide a
+ * jquery selector string that will match on the widget's dom element. If there is no widget provided,
+ * it will match on the document
+ * step: on each click, the target will be scrolled by it's deplayed size multiplied by this value.
+ * duration: this is the duration of the scrolling animation
+ * wheel_step: the target will be scrolled by wheel_step pixels on each mouse scroll.
+ * track_bottom: the target will be kept on bottom when it's on the bottom and the size has changed
+ * on_show: this function will be called with the scrollbar as sole argument when the scrollbar is shown
+ * on_hide: this function will be called with the scrollbar as sole argument when the scrollbar is hidden
+ */
+function openerp_pos_scrollbar(instance, module){ //module is instance.point_of_sale
+
+ module.ScrollbarWidget = instance.web.Widget.extend({
+ template:'ScrollbarWidget',
+
+ init: function(parent,options){
+ var self = this;
+ options = options || {};
+ this._super(parent,options);
+ this.target_widget = options.target_widget;
+ this.target_selector = options.target_selector;
+ this.scroll_target = this.target().scrollTop();
+ this.scroll_step = options.step || 0.8;
+ this.scroll_duration = options.duration || 250;
+ this.wheel_step = options.wheel_step || 80;
+ this.name = options.name || 'unnamed';
+ this.bottom = false; // true if the scroller cannot be scrolled further
+ this.track_bottom = options.track_bottom || false;
+ this.on_show = options.on_show || function(){};
+ this.on_hide = options.on_hide || function(){};
+
+ // these handlers are declared once for the object's lifetime so that we can bind and unbind them.
+ this.resize_handler = function(){
+ setTimeout(function(){
+ if(self.bottom && self.track_bottom){
+ self.set_position(Number.MAX_VALUE);
+ }
+ self.update_scroller_dimensions();
+ self.update_button_status();
+ self.auto_hide(false);
+ },0);
+ };
+ this.target_mousewheel_handler = function(event,delta){
+ self.scroll(delta*self.wheel_step);
+ }
+ },
+
+ renderElement: function(){
+ this._super();
+ var self = this;
+ this.$('.up-button').off('click').click(function(){
+ self.page_up();
+ });
+ this.$('.down-button').off('click').click(function(){
+ self.page_down();
+ });
+ this.update_scroller_dimensions(false);
+ this.update_button_status();
+ this.auto_hide(false);
+ this.$element.bind('mousewheel',function(event,delta){
+ self.scroll(delta*self.wheel_step);
+ });
+ this.$element.bind('click',function(event){
+ var vpos = event.pageY - self.$element.offset().top;
+ var spos = self.scroller_dimensions();
+ if(vpos > spos.bar_pos && vpos < spos.pos){
+ self.page_up();
+ }else if( (vpos < spos.bar_pos + spos.bar_height) &&
+ (vpos > spos.pos + spos.height) ){
+ self.page_down();
+ }
+ });
+ // FIXME: use the event bus to handle window resize events
+ $(window).unbind('resize',this.resize_handler);
+ $(window).bind('resize',this.resize_handler);
+
+ this.target().unbind('mousewheel',this.target_mousweheel_handler);
+ this.target().bind('mousewheel',this.target_mousewheel_handler);
+
+ // because the rendering is asynchronous we must wait for the next javascript update
+ // for good dimensions values
+ setTimeout(function(){
+ self.update_scroller_dimensions(false);
+ self.update_button_status();
+ self.auto_hide(false);
+ },0);
+ },
+
+ // binds the window resize and the target scrolling events.
+ // it is good advice not to bind these multiple_times
+ bind_events:function(){
+ $(window).resize(function(){
+ });
+ this.target().bind('mousewheel',function(event,delta){
+ self.scroll(delta*self.wheel_step);
+ });
+ },
+
+ // shows the scrollbar. if animated is true, it will do it in an animated fashion
+ show: function(animated){ //FIXME: animated show and hide don't work ... ?
+ if(animated){
+ this.$element.show().animate({'width':'48px'}, 500, 'swing');
+ }else{
+ this.$element.show().css('width','48px');
+ }
+ this.on_show(this);
+ },
+
+ // hides the scrollbar. if animated is true, it will do it in a animated fashion
+ hide: function(animated){
+ var self = this;
+ if(animated){
+ this.$element.animate({'width':'0px'}, 500, 'swing', function(){ self.$element.hide();});
+ }else{
+ this.$element.hide().css('width','0px');
+ }
+ this.on_hide(this);
+ },
+
+ // returns the scroller position and other information as a dictionnary with the following fields:
+ // pos: the position in pixels of the top of the scroller starting from the top of the scrollbar
+ // height: the height of the scroller in pixels
+ // bar_pos: the position of the top of the scrollbar's inner region, starting from the top
+ // bar_height: the height of the scrollbar's inner region
+ scroller_dimensions: function(){
+ var target = this.target()[0];
+ var scroller_height = target.clientHeight / target.scrollHeight || 0;
+ var scroller_pos = this.scroll_target / target.scrollHeight || 0;
+ var button_up_height = this.$('.up-button')[0].offsetHeight || 48;
+ var button_down_height = this.$('.down-button')[0].offsetHeight || 48;
+
+ var bar_height = this.$element[0].offsetHeight || 96;
+ var scrollbar_height = bar_height - button_up_height - button_down_height;
+
+ scroller_pos = scroller_pos * scrollbar_height + button_up_height;
+ scroller_height = scroller_height * scrollbar_height;
+
+ return { pos: Math.round(scroller_pos),
+ height: Math.round(scroller_height),
+ bar_pos: button_up_height,
+ bar_height: scrollbar_height };
+ },
+
+
+ //checks if it should show or hide the scrollbar based on the target content and then show or hide it
+ // if animated is true, then the scrollbar will be shown or hidden with an animation
+ auto_hide: function(animated){
+ var target = this.target()[0];
+ if(target.clientHeight && (target.clientHeight === target.scrollHeight)){
+ this.hide(animated);
+ }else{
+ this.show(animated);
+ }
+ },
+
+ //returns the pageup/down scrolling distance in pixels
+ get_scroll_step: function(){
+ var target = this.target()[0];
+ var step = target.clientHeight * this.scroll_step;
+ var c = target.scrollHeight / step;
+ var c = Math.max(1,Math.ceil(c));
+ return target.scrollHeight / c;
+ },
+
+ //sets the scroller to the correct size and position based on the target scrolling status
+ //if animated is true, the scroller will move smoothly to its destination
+ update_scroller_dimensions: function(animated){
+ var dim = this.scroller_dimensions();
+ var target = this.target()[0];
+ if(animated){
+ this.$('.scroller').animate({'top':dim.pos+'px', 'height': dim.height+'px'},this.scroll_duration);
+ }else{
+ this.$('.scroller').css({'top':dim.pos+'px', 'height': dim.height+'px'});
+ }
+ if(this.scroll_target + target.clientHeight >= target.scrollHeight){
+ this.bottom = true;
+ }else{
+ this.bottom = false;
+ }
+ },
+
+ //disable or enable the up/down buttons according to the scrolled position
+ update_button_status: function(){
+ var target = this.target()[0];
+ this.$('.up-button').removeClass('disabled');
+ this.$('.down-button').removeClass('disabled');
+ if(this.scroll_target === 0){
+ this.$('.up-button').addClass('disabled');
+ }
+ if(this.scroll_target + target.clientHeight >= target.scrollHeight){
+ this.$('.down-button').addClass('disabled');
+ }
+ },
+
+ //returns the jquery object of the scrolling target
+ target: function(){
+ if(this.target_widget){
+ if(this.target_selector){
+ return this.target_widget.$(this.target_selector);
+ }else{
+ return this.target_widget.$element;
+ }
+ }else if(this.target_selector){
+ return $(this.target_selector);
+ }else{
+ return undefined;
+ }
+ },
+
+ //scroll one page up
+ page_up: function(){
+ return this.set_position(this.scroll_target - this.get_scroll_step(), true);
+ },
+
+ //scroll one page down
+ page_down: function(){
+ return this.set_position(this.scroll_target + this.get_scroll_step(), true);
+ },
+
+ //scrolls up or down by pixels
+ scroll: function(pixels){
+ return this.set_position(this.scroll_target - pixels, false);
+ },
+
+ //scroll to a specific position (in pixels).
+ //if animated is true, it will do this in an animated fashion with a duration equal to scroll_duration
+ set_position: function(position,animated){
+ var self = this;
+ var target = this.target()[0];
+ var bottom = target.scrollHeight-target.clientHeight;
+ this.scroll_target = Math.max(0,Math.min(bottom,position));
+ if(this.scroll_target === 0){
+ this.position = 'top';
+ }else if(this.scroll_target === 'bottom'){
+ this.position = 'bottom';
+ }else{
+ this.position = 'center';
+ }
+ if(animated){
+ this.target().animate({'scrollTop':this.scroll_target},this.scroll_duration);
+ this.update_button_status();
+ this.update_scroller_dimensions(true);
+ }else{
+ this.target().scrollTop(this.scroll_target);
+ this.update_scroller_dimensions(false);
+ this.update_button_status();
+ }
+ return this.scroll_target;
+ },
+
+ //returns the current position of the scrollbar
+ get_position: function(){
+ return this.scroll_target;
+ },
+
+ //returns true if it cannot be scrolled further down
+ is_at_bottom: function(){
+ return this.bottom;
+ }
+
+ });
+}
diff --git a/addons/point_of_sale/static/src/js/pos_widgets.js b/addons/point_of_sale/static/src/js/pos_widgets.js
new file mode 100644
index 00000000000..8ec2d9c329c
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/pos_widgets.js
@@ -0,0 +1,893 @@
+function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
+ var QWeb = instance.web.qweb;
+
+ module.NumpadWidget = module.PosBaseWidget.extend({
+ template:'NumpadWidget',
+ init: function(parent, options) {
+ this._super(parent);
+ this.state = new module.NumpadState();
+ },
+ start: function() {
+ this.state.bind('change:mode', this.changedMode, this);
+ this.changedMode();
+ this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
+ this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
+ this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
+ this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
+ },
+ clickDeleteLastChar: function() {
+ return this.state.deleteLastChar();
+ },
+ clickSwitchSign: function() {
+ return this.state.switchSign();
+ },
+ clickAppendNewChar: function(event) {
+ var newChar;
+ newChar = event.currentTarget.innerText || event.currentTarget.textContent;
+ return this.state.appendNewChar(newChar);
+ },
+ clickChangeMode: function(event) {
+ var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
+ return this.state.changeMode(newMode);
+ },
+ changedMode: function() {
+ var mode = this.state.get('mode');
+ $('.selected-mode').removeClass('selected-mode');
+ $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
+ },
+ });
+
+ // The paypad allows to select the payment method (cashRegisters)
+ // used to pay the order.
+ module.PaypadWidget = module.PosBaseWidget.extend({
+ template: 'PaypadWidget',
+ renderElement: function() {
+ var self = this;
+ this._super();
+
+ this.pos.get('cashRegisters').each(function(cashRegister) {
+ var button = new module.PaypadButtonWidget(self,{
+ pos: self.pos,
+ pos_widget : self.pos_widget,
+ cashRegister: cashRegister,
+ });
+ button.appendTo(self.$element);
+ });
+ }
+ });
+
+ module.PaypadButtonWidget = module.PosBaseWidget.extend({
+ template: 'PaypadButtonWidget',
+ init: function(parent, options){
+ this._super(parent, options);
+ this.cashRegister = options.cashRegister;
+ },
+ renderElement: function() {
+ var self = this;
+ this._super();
+
+ this.$element.click(function(){
+ if (self.pos.get('selectedOrder').get('screen') === 'receipt'){ //TODO Why ?
+ console.warn('TODO should not get there...?');
+ return;
+ }
+ self.pos.get('selectedOrder').addPaymentLine(self.cashRegister);
+ self.pos_widget.screen_selector.set_current_screen('payment');
+ });
+ },
+ });
+
+ module.OrderlineWidget = module.PosBaseWidget.extend({
+ template: 'OrderlineWidget',
+ init: function(parent, options) {
+ this._super(parent,options);
+
+ this.model = options.model;
+ this.order = options.order;
+
+ this.model.bind('change', _.bind( function() {
+ this.refresh();
+ }, this));
+ },
+ click_handler: function() {
+ this.order.selectLine(this.model);
+ this.on_selected();
+ },
+ renderElement: function() {
+ this._super();
+ this.$element.click(_.bind(this.click_handler, this));
+ if(this.model.is_selected()){
+ this.$element.addClass('selected');
+ }
+ },
+ refresh: function(){
+ this.renderElement();
+ this.on_refresh();
+ },
+ on_selected: function() {},
+ on_refresh: function(){},
+ });
+
+ module.OrderWidget = module.PosBaseWidget.extend({
+ template:'OrderWidget',
+ init: function(parent, options) {
+ this._super(parent,options);
+ this.compact = false;
+ this.set_numpad_state(options.numpadState);
+ this.pos.bind('change:selectedOrder', this.change_selected_order, this);
+ this.bind_orderline_events();
+ },
+ set_numpad_state: function(numpadState) {
+ if (this.numpadState) {
+ this.numpadState.unbind('set_value', this.set_value);
+ }
+ this.numpadState = numpadState;
+ if (this.numpadState) {
+ this.numpadState.bind('set_value', this.set_value, this);
+ this.numpadState.reset();
+ }
+ },
+ set_value: function(val) {
+ var order = this.pos.get('selectedOrder');
+ if (order.get('orderLines').length !== 0) {
+ var mode = this.numpadState.get('mode');
+ if( mode === 'quantity'){
+ order.getSelectedLine().set_quantity(val);
+ }else if( mode === 'discount'){
+ order.getSelectedLine().set_discount(val);
+ }else if( mode === 'list_price'){
+ order.getSelectedLine().set_list_price(val);
+ }
+ } else {
+ this.pos.get('selectedOrder').destroy();
+ }
+ },
+ change_selected_order: function() {
+ this.currentOrderLines.unbind();
+ this.bind_orderline_events();
+ this.renderElement();
+ },
+ bind_orderline_events: function() {
+ this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
+ this.currentOrderLines.bind('add', this.renderElement, this);
+ this.currentOrderLines.bind('remove', this.renderElement, this);
+ },
+ update_numpad: function() {
+ var reset = false;
+ if (this.selected_line !== this.pos.get('selectedOrder').getSelectedLine()) {
+ reset = true;
+ }
+ this.selected_line = this.pos.get('selectedOrder').getSelectedLine();
+ if (reset && this.numpadState)
+ this.numpadState.reset();
+ },
+ renderElement: function() {
+ var self = this;
+ this._super();
+
+ if(!this.compact){
+ $('.point-of-sale .order-container').css({'bottom':'0px'});
+ }
+
+ var $content = this.$('.orderlines');
+ this.currentOrderLines.each(_.bind( function(orderLine) {
+ var line = new module.OrderlineWidget(this, {
+ model: orderLine,
+ order: this.pos.get('selectedOrder'),
+ });
+ line.on_selected.add(_.bind(this.update_numpad, this));
+ line.on_refresh.add(_.bind(this.update_summary, this));
+ line.appendTo($content);
+ }, this));
+ this.update_numpad();
+ this.update_summary();
+
+ var position = this.scrollbar ? this.scrollbar.get_position() : 0;
+ var at_bottom = this.scrollbar ? this.scrollbar.is_at_bottom() : false;
+
+ this.scrollbar = new module.ScrollbarWidget(this,{
+ target_widget: this,
+ target_selector: '.order-scroller',
+ name: 'order',
+ track_bottom: true,
+ on_show: function(){
+ self.$('.order-scroller').css({'width':'89%'},100);
+ },
+ on_hide: function(){
+ self.$('.order-scroller').css({'width':'100%'},100);
+ },
+ });
+
+ this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget'));
+ this.scrollbar.set_position(position);
+
+ if( at_bottom ){
+ this.scrollbar.set_position(Number.MAX_VALUE, false);
+ }
+
+ },
+ update_summary: function(){
+ var order = this.pos.get('selectedOrder');
+ var total = order ? order.getTotal() : 0;
+ this.$('.summary .value.total').html(this.format_currency(total));
+ },
+ set_compact: function(compact){
+ if(this.compact !== compact){
+ this.compact = compact;
+ this.renderElement();
+ }
+ },
+ });
+
+ module.ProductWidget = module.PosBaseWidget.extend({
+ template: 'ProductWidget',
+ init: function(parent, options) {
+ this._super(parent,options);
+ this.model = options.model;
+ this.model.attributes.weight = options.weight;
+ this.next_screen = options.next_screen;
+ this.click_product_action = options.click_product_action;
+ },
+ add_to_order: function(event) {
+ /* Preserve the category URL */
+ event.preventDefault();
+ return (this.pos.get('selectedOrder')).addProduct(this.model);
+ },
+ set_weight: function(weight){
+ this.model.attributes.weight = weight;
+ this.renderElement();
+ },
+ renderElement: function() {
+ this._super();
+ var self = this;
+ $("a", this.$element).click(function(e){
+ if(self.click_product_action){
+ self.click_product_action(self.model);
+ }
+ });
+ },
+ });
+
+ module.PaymentlineWidget = module.PosBaseWidget.extend({
+ template: 'PaymentlineWidget',
+ init: function(parent, options) {
+ this._super(parent,options);
+ this.payment_line = options.payment_line;
+ this.payment_line.bind('change', this.changedAmount, this);
+ },
+ on_delete: function() {},
+ changeAmount: function(event) {
+ var newAmount;
+ newAmount = event.currentTarget.value;
+ if (newAmount && !isNaN(newAmount)) {
+ this.amount = parseFloat(newAmount);
+ this.payment_line.set_amount(this.amount);
+ }
+ },
+ changedAmount: function() {
+ if (this.amount !== this.payment_line.get_amount())
+ this.renderElement();
+ },
+ renderElement: function() {
+ this.name = this.payment_line.get_cashregister().get('journal_id')[1];
+ this._super();
+ this.$('input').keyup(_.bind(this.changeAmount, this));
+ this.$('.delete-payment-line').click(this.on_delete);
+ },
+ });
+
+ module.OrderButtonWidget = module.PosBaseWidget.extend({
+ template:'OrderButtonWidget',
+ init: function(parent, options) {
+ this._super(parent,options);
+ this.order = options.order;
+ this.order.bind('destroy', _.bind( function() {
+ this.destroy();
+ }, this));
+ this.pos.bind('change:selectedOrder', _.bind( function(pos) {
+ var selectedOrder;
+ selectedOrder = pos.get('selectedOrder');
+ if (this.order === selectedOrder) {
+ this.setButtonSelected();
+ }
+ }, this));
+ },
+ renderElement:function(){
+ this._super();
+ this.$('button.select-order').click(_.bind(this.selectOrder, this));
+ this.$('button.close-order').click(_.bind(this.closeOrder, this));
+ },
+ selectOrder: function(event) {
+ this.pos.set({
+ selectedOrder: this.order
+ });
+ },
+ setButtonSelected: function() {
+ $('.selected-order').removeClass('selected-order');
+ this.$element.addClass('selected-order');
+ },
+ closeOrder: function(event) {
+ this.order.destroy();
+ },
+ });
+
+ module.ActionButtonWidget = instance.web.Widget.extend({
+ template:'ActionButtonWidget',
+ init: function(parent, options){
+ this._super(parent, options);
+ this.label = options.label || 'button';
+ this.rightalign = options.rightalign || false;
+ this.click_action = options.click;
+ if(options.icon){
+ this.icon = options.icon;
+ this.template = 'ActionButtonWidgetWithIcon';
+ }
+ },
+ renderElement: function(){
+ this._super();
+ if(this.click_action){
+ this.$element.click(_.bind(this.click_action, this));
+ }
+ },
+ });
+
+ module.ActionBarWidget = instance.web.Widget.extend({
+ template:'ActionBarWidget',
+ init: function(parent, options){
+ this._super(parent,options);
+ this.button_list = [];
+ this.fake_buttons = {};
+ this.visibility = {};
+ },
+ set_element_visible: function(element, visible, action){
+ if(visible != this.visibility[element]){
+ this.visibility[element] = visible;
+ if(visible){
+ this.$('.'+element).show();
+ }else{
+ this.$('.'+element).hide();
+ }
+ }
+ if(visible && action){
+ this.$('.'+element).off('click').click(action);
+ }
+ },
+ destroy_buttons:function(){
+ for(var i = 0; i < this.button_list.length; i++){
+ this.button_list[i].destroy();
+ }
+ this.button_list = [];
+ return this;
+ },
+ add_new_button: function(button_options){
+ if(arguments.length == 1){
+ var button = new module.ActionButtonWidget(this,button_options);
+ this.button_list.push(button);
+ button.appendTo($('.pos-actionbar-button-list'));
+ return button;
+ }else{
+ for(var i = 0; i < arguments.length; i++){
+ this.add_new_button(arguments[i]);
+ }
+ }
+ return undefined;
+ },
+ });
+
+ module.ProductCategoriesWidget = module.PosBaseWidget.extend({
+ template: 'ProductCategoriesWidget',
+ init: function(parent, options){
+ var self = this;
+ this._super(parent,options);
+ this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
+ this.onlyWeightable = options.onlyWeightable || false;
+ this.category = this.pos.root_category;
+ this.breadcrumb = [];
+ this.subcategories = [];
+ this.set_category();
+ },
+
+ // changes the category. if undefined, sets to root category
+ set_category : function(category){
+ if(!category){
+ this.category = this.pos.root_category;
+ }else{
+ this.category = category;
+ }
+
+ this.breadcrumb = [];
+ for(var i = 1; i < this.category.ancestors.length; i++){
+ this.breadcrumb.push(this.category.ancestors[i]);
+ }
+ if(this.category !== this.pos.root_category){
+ this.breadcrumb.push(this.category);
+ }
+ if(this.product_type === 'weightable'){
+ this.subcategories = [];
+ for(var i = 0; i < this.category.childrens.length; i++){
+ if(this.category.childrens[i].weightable_product_list.length > 0){
+ this.subcategories.push( this.category.childrens[i]);
+ }
+ }
+ }else{
+ this.subcategories = this.category.childrens || [];
+ }
+ },
+
+ renderElement: function(){
+ var self = this;
+ this._super();
+ _.each(this.subcategories, function(category){
+ var button = QWeb.render('CategoryButton',{category:category});
+ button = _.str.trim(button);
+
+ $(button).appendTo(this.$('.category-list')).click(function(event){
+ var id = category.id;
+ var cat = self.pos.categories_by_id[id];
+ self.set_category(cat);
+ self.renderElement();
+ self.search_and_categories(cat);
+ });
+ });
+ // breadcrumb click actions
+ this.$(".oe-pos-categories-list a").click(function(event){
+ var id = $(event.target).data("category-id");
+ var category = self.pos.categories_by_id[id];
+ self.set_category(category);
+ self.renderElement();
+ self.search_and_categories(category);
+ });
+ this.search_and_categories();
+ },
+
+ set_product_type: function(type){ // 'all' | 'weightable'
+ this.product_type = type;
+ this.reset_category();
+ },
+
+ // resets the current category to the root category
+ reset_category: function(){
+ this.set_category();
+ this.renderElement();
+ this.search_and_categories();
+ },
+
+ // filters the products, and sets up the search callbacks
+ search_and_categories: function(category){
+ var self = this;
+
+ var all_products = this.pos.get('product_list');
+ var all_packages = this.pos.get('product.packaging');
+
+ // find all products belonging to the current category
+ var products = [];
+ if(this.product_type === 'weightable'){
+ products = all_products.filter( function(product){
+ return self.category.weightable_product_set[product.id];
+ });
+ }else{
+ products = all_products.filter( function(product){
+ return self.category.product_set[product.id];
+ });
+ }
+
+ // product lists watch for reset events on 'products' to re-render.
+ // FIXME that means all productlist widget re-render... even the hidden ones !
+ this.pos.get('products').reset(products);
+
+ // find all the products whose name match the query in the searchbox
+ this.$('.searchbox input').keyup(function(){
+ var results, search_str;
+ search_str = $(this).val().toLowerCase();
+ if(search_str){
+ results = products.filter( function(p){
+ return p.name.toLowerCase().indexOf(search_str) != -1 ||
+ (p.ean13 && p.ean13.indexOf(search_str) != -1);
+ });
+ self.$element.find('.search-clear').fadeIn();
+ }else{
+ results = products;
+ self.$element.find('.search-clear').fadeOut();
+ }
+ self.pos.get('products').reset(results);
+ });
+ this.$('.searchbox input').click(function(){
+ });
+
+ //reset the search when clicking on reset
+ this.$('.search-clear').click(function(){
+ self.pos.get('products').reset(products);
+ self.$('.searchbox input').val('').focus();
+ self.$('.search-clear').fadeOut();
+ });
+ },
+ });
+
+ module.ProductListWidget = module.ScreenWidget.extend({
+ template:'ProductListWidget',
+ init: function(parent, options) {
+ var self = this;
+ this._super(parent,options);
+ this.model = options.model;
+ this.product_list = [];
+ this.weight = options.weight || 0;
+ this.show_scale = options.show_scale || false;
+ this.next_screen = options.next_screen || false;
+ this.click_product_action = options.click_product_action;
+
+ this.pos.get('products').bind('reset', function(){
+ self.renderElement();
+ });
+ },
+ set_weight: function(weight){
+ for(var i = 0; i < this.product_list.length; i++){
+ this.product_list[i].set_weight(weight);
+ }
+ },
+ renderElement: function() {
+ var self = this;
+ this._super();
+ this.product_list = [];
+ this.pos.get('products')
+ .chain()
+ .map(function(product) {
+ var product = new module.ProductWidget(self, {
+ model: product,
+ weight: self.weight,
+ click_product_action: self.click_product_action,
+ })
+ self.product_list.push(product);
+ return product;
+ })
+ .invoke('appendTo', this.$('.product-list'));
+
+ this.scrollbar = new module.ScrollbarWidget(this,{
+ target_widget: this,
+ target_selector: '.product-list-scroller',
+ on_show: function(){
+ self.$('.product-list-scroller').css({'padding-right':'62px'},100);
+ },
+ on_hide: function(){
+ self.$('.product-list-scroller').css({'padding-right':'0px'},100);
+ },
+ });
+
+ this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget'));
+
+ },
+ });
+
+ module.UsernameWidget = module.PosBaseWidget.extend({
+ template: 'UsernameWidget',
+ init: function(parent, options){
+ var options = options || {};
+ this._super(parent,options);
+ this.mode = options.mode || 'cashier';
+ },
+ set_user_mode: function(mode){
+ this.mode = mode;
+ this.refresh();
+ },
+ refresh: function(){
+ this.renderElement();
+ },
+ get_name: function(){
+ var user;
+ if(this.mode === 'cashier'){
+ user = this.pos.get('cashier') || this.pos.get('user');
+ }else{
+ user = this.pos.get('selectedOrder').get_client() || this.pos.get('user');
+ }
+ if(user){
+ return user.name;
+ }else{
+ return "";
+ }
+ },
+ });
+
+// ---------- 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
+ // - a leftpane, containing the list of bought products (orderlines)
+ // - a rightpane, containing the screens (see pos_screens.js)
+ // - an actionbar on the bottom, containing various action buttons
+ // - popups
+ // - an onscreen keyboard
+ // a screen_selector which controls the switching between screens and the showing/closing of popups
+
+ module.PosWidget = module.PosBaseWidget.extend({
+ template: 'PosWidget',
+ init: function() {
+ this._super(arguments[0],{});
+
+ this.pos = new module.PosModel(this.session);
+ this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
+
+ this.numpad_visible = true;
+ this.leftpane_visible = true;
+ this.leftpane_width = '440px';
+ this.cashier_controls_visible = true;
+ },
+
+ start: function() {
+ var self = this;
+ return self.pos.ready.then(function() {
+ self.build_currency_template();
+ self.renderElement();
+
+ self.$('.neworder-button').click(_.bind(self.create_new_order, self));
+
+ //when a new order is created, add an order button widget
+ self.pos.get('orders').bind('add', function(new_order){
+ var new_order_button = new module.OrderButtonWidget(null, {
+ order: new_order,
+ pos: self.pos
+ });
+ new_order_button.appendTo($('#orders'));
+ new_order_button.selectOrder();
+ }, self);
+
+ self.pos.get('orders').add(new module.Order({ pos: self.pos }));
+
+ self.build_widgets();
+
+ instance.webclient.set_content_full_screen(true);
+
+ if (!self.pos.get('pos_session')) {
+ self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
+ }else if(!self.pos.get('pos_config')){
+ self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
+ }
+
+ self.$('.loader').animate({opacity:0},3000,'swing',function(){$('.loader').hide();});
+ self.$('.loader img').hide();
+
+ },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'])
+ .pipe( _.bind(function(res){
+ return instance.connection.rpc('/web/action/load', {'action_id': res[0]['res_id']})
+ .pipe(_.bind(function(result){
+ var action = result.result;
+ this.do_action(action);
+ }, this));
+ }, self));
+ });
+ },
+
+ build_widgets: function() {
+
+ // -------- Screens ---------
+
+ this.product_screen = new module.ProductScreenWidget(this,{});
+ this.product_screen.appendTo($('#rightpane'));
+
+ this.receipt_screen = new module.ReceiptScreenWidget(this, {});
+ this.receipt_screen.appendTo($('#rightpane'));
+
+ this.payment_screen = new module.PaymentScreenWidget(this, {});
+ this.payment_screen.appendTo($('#rightpane'));
+
+ this.welcome_screen = new module.WelcomeScreenWidget(this,{});
+ this.welcome_screen.appendTo($('#rightpane'));
+
+ this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
+ this.client_payment_screen.appendTo($('#rightpane'));
+
+ this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
+ this.scale_invite_screen.appendTo($('#rightpane'));
+
+ this.scale_screen = new module.ScaleScreenWidget(this,{});
+ this.scale_screen.appendTo($('#rightpane'));
+
+ // -------- Popups ---------
+
+ this.help_popup = new module.HelpPopupWidget(this, {});
+ this.help_popup.appendTo($('.point-of-sale'));
+
+ this.error_popup = new module.ErrorPopupWidget(this, {});
+ this.error_popup.appendTo($('.point-of-sale'));
+
+ this.error_product_popup = new module.ErrorProductNotRecognizedPopupWidget(this, {});
+ this.error_product_popup.appendTo($('.point-of-sale'));
+
+ this.error_session_popup = new module.ErrorNoSessionPopupWidget(this, {});
+ this.error_session_popup.appendTo($('.point-of-sale'));
+
+ // -------- Misc ---------
+
+ this.notification = new module.SynchNotificationWidget(this,{});
+ this.notification.replace(this.$('.placeholder-SynchNotificationWidget'));
+
+ this.username = new module.UsernameWidget(this,{});
+ this.username.replace(this.$('.placeholder-UsernameWidget'));
+
+ this.action_bar = new module.ActionBarWidget(this);
+ this.action_bar.appendTo($(".point-of-sale #content"));
+
+ this.paypad = new module.PaypadWidget(this, {});
+ this.paypad.replace($('#placeholder-PaypadWidget'));
+
+ this.numpad = new module.NumpadWidget(this);
+ this.numpad.replace($('#placeholder-NumpadWidget'));
+
+ this.order_widget = new module.OrderWidget(this, {});
+ this.order_widget.replace($('#placeholder-OrderWidget'));
+
+ this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
+ 'keyboard_model': 'simple'
+ });
+ this.onscreen_keyboard.appendTo($(".point-of-sale #content"));
+
+ // -------- Screen Selector ---------
+
+ this.screen_selector = new module.ScreenSelector({
+ pos: this.pos,
+ screen_set:{
+ 'products': this.product_screen,
+ 'payment' : this.payment_screen,
+ 'client_payment' : this.client_payment_screen,
+ 'scale_invite' : this.scale_invite_screen,
+ 'scale': this.scale_screen,
+ 'receipt' : this.receipt_screen,
+ 'welcome' : this.welcome_screen,
+ },
+ popup_set:{
+ 'help': this.help_popup,
+ 'error': this.error_popup,
+ 'error-product': this.error_product_popup,
+ 'error-session': this.error_session_popup,
+ },
+ default_client_screen: 'welcome',
+ default_cashier_screen: 'products',
+ default_mode: this.pos.use_selfcheckout ? 'client' : 'cashier',
+ });
+
+ this.screen_selector.set_default_screen();
+
+ this.pos.barcode_reader.connect();
+ },
+
+ //FIXME this method is probably not at the right place ...
+ scan_product: function(parsed_ean){
+ var selectedOrder = this.pos.get('selectedOrder');
+ var scannedProductModel = this.get_product_by_ean(parsed_ean);
+ if (!scannedProductModel){
+ return false;
+ } else {
+ if(parsed_ean.type === 'price'){
+ selectedOrder.addProduct(new module.Product(scannedProductModel), { price:parsed_ean.value});
+ }else if(parsed_ean.type === 'weight'){
+ selectedOrder.addProduct(new module.Product(scannedProductModel), { quantity:parsed_ean.value, merge:false});
+ }else{
+ selectedOrder.addProduct(new module.Product(scannedProductModel));
+ }
+ return true;
+ }
+ },
+
+ get_product_by_ean: function(parsed_ean) {
+ var allProducts = this.pos.get('product_list');
+ var allPackages = this.pos.get('product.packaging');
+ var scannedProductModel = undefined;
+
+ if (parsed_ean.type === 'price' || parsed_ean.type === 'weight') {
+ var itemCode = parsed_ean.id;
+ var scannedPackaging = _.detect(allPackages, function(pack) {
+ return pack.ean && pack.ean.substring(0,7) === itemCode;
+ });
+ if (scannedPackaging) {
+ scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
+ }else{
+ scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 && (pc.ean13.substring(0,7) === parsed_ean.id);});
+ }
+ } else if(parsed_ean.type === 'unit'){
+ scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === parsed_ean.ean;}); //TODO DOES NOT SCALE
+ }
+ return scannedProductModel;
+ },
+ // creates a new order, and add it to the list of orders.
+ create_new_order: function() {
+ var new_order;
+ new_order = new module.Order({ pos: this.pos });
+ this.pos.get('orders').add(new_order);
+ this.pos.set({ selectedOrder: new_order });
+ },
+ changed_pending_operations: function () {
+ var self = this;
+ this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
+ },
+ // shows or hide the numpad and related controls like the paypad.
+ set_numpad_visible: function(visible){
+ if(visible !== this.numpad_visible){
+ this.numpad_visible = visible;
+ if(visible){
+ this.numpad.show();
+ this.paypad.show();
+ this.order_widget.set_compact(true);
+ }else{
+ this.numpad.hide();
+ this.paypad.hide();
+ this.order_widget.set_compact(false);
+ }
+ }
+ },
+ //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
+ set_leftpane_visible: function(visible){
+ if(visible !== this.leftpane_visible){
+ this.leftpane_visible = visible;
+ if(visible){
+ $('#leftpane').show().animate({'width':this.leftpane_width},500,'swing');
+ $('#rightpane').animate({'left':this.leftpane_width},500,'swing');
+ }else{
+ var leftpane = $('#leftpane');
+ $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.hide(); });
+ $('#rightpane').animate({'left':'0px'},500,'swing');
+ }
+ }
+ },
+ //shows or hide the controls in the PosWidget that are specific to the cashier ( Orders, close button, etc. )
+ set_cashier_controls_visible: function(visible){
+ if(visible !== this.cashier_controls_visible){
+ this.cashier_controls_visible = visible;
+ if(visible){
+ $('#loggedas').show();
+ $('#rightheader').show();
+ }else{
+ $('#loggedas').hide();
+ $('#rightheader').hide();
+ }
+ }
+ },
+ try_close: function() {
+ var self = this;
+ self.pos.flush().then(function() {
+ self.close();
+ });
+ },
+ close: function() {
+ this.pos.barcode_reader.disconnect();
+ return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
+ _.bind(function(res) {
+ return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
+ var action = result.result;
+ action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
+ this.do_action(action);
+ }, this));
+ }, this));
+ },
+ destroy: function() {
+ instance.webclient.set_content_full_screen(false);
+ self.pos = undefined;
+ this._super();
+ }
+ });
+}
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index ee2dee7c875..29dc0fdf473 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -2,158 +2,105 @@
-
-
-
-
-
-
-
-
- Close
-
-
-
- +
-
-
-
-
-
-
-
-
-
-
-
Product
-
Price
-
Disc (%)
-
Qty
-
Total
-
-
-
-
+
+
+
+
+
+
-
- VAT
+ Tax
Quantity
@@ -236,7 +236,7 @@
Description
- VAT
+ Tax
Quantity
diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py
index d43bc75be7a..45c01c582a0 100644
--- a/addons/sale/res_config.py
+++ b/addons/sale/res_config.py
@@ -69,6 +69,9 @@ class sale_configuration(osv.osv_memory):
'group_sale_delivery_address': fields.boolean("Allow Different Addresses for Delivery and Invoice",
implied_group='sale.group_delivery_invoice_address',
help="Allows you to specify different delivery and invoice addresses on a sale order."),
+ 'group_mrp_properties': fields.boolean('Properties on Lines',
+ implied_group='sale.group_mrp_properties',
+ help="Allows you to tag sale order lines with properties."),
'group_discount_per_so_line': fields.boolean("Discount per Line",
implied_group='sale.group_discount_per_so_line',
help="Allows you to apply some discount per sale order line."),
diff --git a/addons/sale/res_config_view.xml b/addons/sale/res_config_view.xml
index 2078c141b69..ee05a6d2d26 100644
--- a/addons/sale/res_config_view.xml
+++ b/addons/sale/res_config_view.xml
@@ -8,45 +8,35 @@
form
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -61,13 +51,14 @@
-
+ 0
-
-
+ 2
+
+
-
+
diff --git a/addons/sale/res_partner_view.xml b/addons/sale/res_partner_view.xml
index bb5695eac39..76f6f3ad816 100644
--- a/addons/sale/res_partner_view.xml
+++ b/addons/sale/res_partner_view.xml
@@ -1,23 +1,57 @@
-
+
+ Quotations and Sales
+ sale.order
+ form
+ tree,form,graph
+ {'search_default_partner_id': active_id}
+
+
+ This customer has no quotation or sale order.
+ Click here to create a new quotation.
+ <p>
+ The "Quotation" is the first step of the Sales flow. Manage your sales from quotation to invoice.
+ <p>
+ You will be able to sell products (manage deliveries) as well as services (create projects).
+
+
+
+
res.partner.kanban.saleorder.inheritres.partnerkanban
+
-
-
+
+ Sales
+
+
+ res.partner.view.buttons
+ res.partner
+ form
+
+
+
+
+
+
+
+
+
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index aeede7e5533..f920b34ca42 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -483,19 +483,19 @@ class sale_order(osv.osv):
return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True}
def manual_invoice(self, cr, uid, ids, context=None):
+ """ create invoices for the given sale orders (ids), and open the form
+ view of one of the newly created invoices
+ """
mod_obj = self.pool.get('ir.model.data')
wf_service = netsvc.LocalService("workflow")
- inv_ids = set()
- inv_ids1 = set()
- for id in ids:
- for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
- inv_ids.add(record.id)
- # inv_ids would have old invoices if any
+
+ # create invoices through the sale orders' workflow
+ inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
for id in ids:
wf_service.trg_validate(uid, 'sale.order', id, 'manual_invoice', cr)
- for record in self.pool.get('sale.order').browse(cr, uid, id).invoice_ids:
- inv_ids1.add(record.id)
- inv_ids = list(inv_ids1.difference(inv_ids))
+ inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
+ # determine newly created invoices
+ new_inv_ids = list(inv_ids1 - inv_ids0)
res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
res_id = res and res[1] or False,
@@ -510,7 +510,7 @@ class sale_order(osv.osv):
'type': 'ir.actions.act_window',
'nodestroy': True,
'target': 'current',
- 'res_id': inv_ids and inv_ids[0] or False,
+ 'res_id': new_inv_ids and new_inv_ids[0] or False,
}
def action_view_invoice(self, cr, uid, ids, context=None):
diff --git a/addons/sale/sale_data.xml b/addons/sale/sale_data.xml
index 4622577ac3d..c93651219af 100644
--- a/addons/sale/sale_data.xml
+++ b/addons/sale/sale_data.xml
@@ -1,17 +1,13 @@
-
+
Sales Ordersale.order
-
+
@@ -26,5 +22,19 @@
+
+
+
+
+ Module Sales Management has been installed
+ From the top menu Sales, manage your customers, quotations and
+ sales orders. With sale orders, you can manage product
+ deliveries and customer invoices.
+
+ If you need to manage your sales pipeline (leads, opportunities,
+ phonecalls), you can install the module CRM from the top menu
+ Settings.
+
+
diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml
index e7420edb910..ac665df7a6b 100644
--- a/addons/sale/sale_view.xml
+++ b/addons/sale/sale_view.xml
@@ -1,6 +1,16 @@
+
-
+
+
+
+
+
@@ -146,16 +156,16 @@
-
-
-
-
+
+
+
-
+
@@ -171,10 +181,9 @@
-
+
-
@@ -190,14 +199,14 @@
@@ -297,7 +323,7 @@
-
+
@@ -443,7 +469,7 @@
-
+
@@ -569,14 +595,6 @@
-
-
+
+ Properties on lines
+
+
+
Discount on lines
diff --git a/addons/sale/stock_view.xml b/addons/sale/stock_view.xml
index 242d2c4cef6..40de6872be0 100644
--- a/addons/sale/stock_view.xml
+++ b/addons/sale/stock_view.xml
@@ -7,9 +7,12 @@
form
-
+
+
@@ -19,8 +22,8 @@
form
-
-
+
+
@@ -71,7 +74,7 @@
formtree,form,calendar[('type','=','out')]
- {'default_type': 'out', 'contact_display': 'partner_address', 'search_default_to_invoice': 1, 'search_default_done': 1}
+ {'default_type': 'out', 'contact_display': 'partner_address', 'search_default_to_invoice': 1, 'search_default_done': 1, 'default_invoice_state': '2binvoiced'}
diff --git a/addons/sale/test/manual_order_policy.yml b/addons/sale/test/manual_order_policy.yml
index bb197763486..505ab0933d0 100644
--- a/addons/sale/test/manual_order_policy.yml
+++ b/addons/sale/test/manual_order_policy.yml
@@ -16,7 +16,7 @@
ctx = context.copy()
ctx.update({"active_model": 'sale.order', "active_ids": [ref("order4")], "active_id":ref("order4")})
order_line = self.pool.get('sale.order.line').browse(cr, uid, ref("line7"), context=context)
- pay_id = self.create(cr, uid, {'product_id': order_line.product_id.id, 'amount': order_line.price_subtotal, 'qtty': order_line.product_uom_qty})
+ pay_id = self.create(cr, uid, {'advance_payment_method': 'fixed', 'product_id': order_line.product_id.id, 'amount': order_line.price_subtotal, 'qtty': order_line.product_uom_qty})
self.create_invoices(cr, uid, [pay_id], context=ctx)
-
I check Invoice which made advance
diff --git a/addons/sale/wizard/sale_make_invoice.py b/addons/sale/wizard/sale_make_invoice.py
index 5265afb9bff..b4b3772a91a 100644
--- a/addons/sale/wizard/sale_make_invoice.py
+++ b/addons/sale/wizard/sale_make_invoice.py
@@ -68,4 +68,4 @@ class sale_make_invoice(osv.osv_memory):
sale_make_invoice()
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py
index f3d9bd65a91..39999a8d1b7 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.py
+++ b/addons/sale/wizard/sale_make_invoice_advance.py
@@ -26,189 +26,198 @@ class sale_advance_payment_inv(osv.osv_memory):
_name = "sale.advance.payment.inv"
_description = "Sales Advance Payment Invoice"
- def _default_product_id(self, cr, uid, context=None):
- try:
- product_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'advance_product_0')
- except ValueError:
- #a ValueError is returned if the xml id given is not found in the table ir_model_data
- return False
- return product_id[1]
-
_columns = {
- 'product_id': fields.many2one('product.product', 'Advance Product', help="Select a product of type service which is called 'Advance Product'. You may have to create it and set it as a default value on this field."),
- 'amount': fields.float('Advance Amount', digits_compute= dp.get_precision('Sale Price'), required=True, help="The amount to be invoiced in advance."),
+ 'advance_payment_method':fields.selection(
+ [('all', 'All'), ('percentage','Percentage'), ('fixed','Fixed Price'),
+ ('lines', 'Some Order Lines')],
+ 'Type', required=True,
+ help="""Use All to create the final invoice.
+ Use Percentage to invoice a percentage of the total amount.
+ Use Fixed Price to invoice a specific amound in advance.
+ Use Some Order Lines to invoice a selection of the sale order lines."""),
'qtty': fields.float('Quantity', digits=(16, 2), required=True),
- 'advance_payment_method':fields.selection([('percentage','Percentage'), ('fixed','Fixed Price')], 'Type', required=True, help="Use Fixed Price if you want to give specific amound in Advance, Use Percentage if you want to give percentage of Total Invoice Amount."),
+ 'product_id': fields.many2one('product.product', 'Advance Product',
+ help="""Select a product of type service which is called 'Advance Product'.
+ You may have to create it and set it as a default value on this field."""),
+ 'amount': fields.float('Advance Amount', digits_compute= dp.get_precision('Sale Price'),
+ help="The amount to be invoiced in advance."),
}
+ def _get_advance_product(self, cr, uid, context=None):
+ try:
+ product = self.pool.get('ir.model.data').get_object(cr, uid, 'sale', 'advance_product_0')
+ except ValueError:
+ # a ValueError is returned if the xml id given is not found in the table ir_model_data
+ return False
+ return product.id
+
_defaults = {
+ 'advance_payment_method': 'all',
'qtty': 1.0,
- 'advance_payment_method': 'fixed',
- 'product_id': _default_product_id,
+ 'product_id': _get_advance_product,
}
- def onchange_advance_payment_method(self, cr, uid, ids, advance_payment_method, product_id, context=None):
+ def onchange_method(self, cr, uid, ids, advance_payment_method, product_id, context=None):
if advance_payment_method == 'percentage':
return {'value': {'amount':0, 'product_id':False }}
- if not product_id:
- return {'value': {'amount': 0}}
- product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
- return {'value': {'amount': product.list_price}}
-
+ if product_id:
+ product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+ return {'value': {'amount': product.list_price}}
+ return {'value': {'amount': 0}}
def create_invoices(self, cr, uid, ids, context=None):
- """
- To create invoices.
-
- @param self: The object pointer.
- @param cr: A database cursor
- @param uid: ID of the user currently logged in
- @param ids: the ID or list of IDs if we want more than one
- @param context: A standard dictionary
-
- @return:
-
- """
- list_inv = []
- obj_sale = self.pool.get('sale.order')
- obj_lines = self.pool.get('account.invoice.line')
- inv_obj = self.pool.get('account.invoice')
+ """ create invoices for the active sale orders """
if context is None:
context = {}
+ wizard = self.browse(cr, uid, ids[0], context)
+ sale_ids = context.get('active_ids', [])
- for sale_adv_obj in self.browse(cr, uid, ids, context=context):
- for sale in obj_sale.browse(cr, uid, context.get('active_ids', []), context=context):
- create_ids = []
- ids_inv = []
- if sale.order_policy == 'postpaid':
- raise osv.except_osv(
- _('Error'),
- _("You cannot make an advance on a sales order \
- that is defined as 'Automatic Invoice after delivery'."))
- val = obj_lines.product_id_change(cr, uid, [], sale_adv_obj.product_id.id,
- uom = False, partner_id = sale.partner_id.id, fposition_id = sale.fiscal_position.id)
- res = val['value']
+ if wizard.advance_payment_method == 'all':
+ # create the final invoices of the active sale orders
+ res = self.pool.get('sale.order').manual_invoice(cr, uid, sale_ids, context)
+ if context.get('open_invoices', False):
+ return res
+ return {'type': 'ir.actions.act_window_close'}
- if not sale_adv_obj.product_id.id :
- prop = self.pool.get('ir.property').get(cr, uid,
- 'property_account_income_categ', 'product.category',
- context=context)
- account_id = prop and prop.id or False
- account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, sale.fiscal_position.id or False, account_id)
- if not account_id:
- raise osv.except_osv(_('Configuration Error !'),
- _('There is no income account defined as global property.'))
- res['account_id'] = account_id
+ if wizard.advance_payment_method == 'lines':
+ # open the list view of sale order lines to invoice
+ act_window = self.pool.get('ir.actions.act_window')
+ res = act_window.for_xml_id(cr, uid, 'sale', 'action_order_line_tree2', context)
+ res['context'] = {
+ 'search_default_uninvoiced': 1,
+ 'search_default_order_id': sale_ids and sale_ids[0] or False,
+ }
+ return res
- if not res.get('account_id'):
+ assert wizard.advance_payment_method in ('fixed', 'percentage')
+
+ sale_obj = self.pool.get('sale.order')
+ inv_obj = self.pool.get('account.invoice')
+ inv_line_obj = self.pool.get('account.invoice.line')
+ inv_ids = []
+
+ for sale in sale_obj.browse(cr, uid, sale_ids, context=context):
+ if sale.order_policy == 'postpaid':
+ raise osv.except_osv(
+ _('Error'),
+ _("You cannot make an advance on a sales order \
+ that is defined as 'Automatic Invoice after delivery'."))
+
+ val = inv_line_obj.product_id_change(cr, uid, [], wizard.product_id.id,
+ uom=False, partner_id=sale.partner_id.id, fposition_id=sale.fiscal_position.id)
+ res = val['value']
+
+ # determine and check income account
+ if not wizard.product_id.id :
+ prop = self.pool.get('ir.property').get(cr, uid,
+ 'property_account_income_categ', 'product.category', context=context)
+ prop_id = prop and prop.id or False
+ account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, sale.fiscal_position.id or False, prop_id)
+ if not account_id:
raise osv.except_osv(_('Configuration Error !'),
- _('There is no income account defined ' \
- 'for this product: "%s" (id:%d)') % \
- (sale_adv_obj.product_id.name, sale_adv_obj.product_id.id,))
+ _('There is no income account defined as global property.'))
+ res['account_id'] = account_id
+ if not res.get('account_id'):
+ raise osv.except_osv(_('Configuration Error !'),
+ _('There is no income account defined for this product: "%s" (id:%d)') % \
+ (wizard.product_id.name, wizard.product_id.id,))
- final_amount = 0
- if sale_adv_obj.amount <= 0.00:
- raise osv.except_osv(_('Data Insufficient !'),
- _('Please check the Advance Amount, it should not be 0 or less!'))
- if sale_adv_obj.advance_payment_method == 'percentage':
- final_amount = sale.amount_total * sale_adv_obj.amount / 100
- if not res.get('name'):
- res['name'] = _("Advance of %s %%") % (sale_adv_obj.amount)
- else:
- final_amount = sale_adv_obj.amount
- if not res.get('name'):
- #TODO: should find a way to call formatLang() from rml_parse
- if sale.pricelist_id.currency_id.position == 'after':
- res['name'] = _("Advance of %s %s") % (final_amount, sale.pricelist_id.currency_id.symbol)
- else:
- res['name'] = _("Advance of %s %s") % (sale.pricelist_id.currency_id.symbol, final_amount)
+ # determine invoice amount
+ if wizard.amount <= 0.00:
+ raise osv.except_osv(_('Incorrect Data'),
+ _('The value of Advance Amount must be positive.'))
+ if wizard.advance_payment_method == 'percentage':
+ inv_amount = sale.amount_total * wizard.amount / 100
+ if not res.get('name'):
+ res['name'] = _("Advance of %s %%") % (wizard.amount)
+ else:
+ inv_amount = wizard.amount
+ if not res.get('name'):
+ #TODO: should find a way to call formatLang() from rml_parse
+ symbol = sale.pricelist_id.currency_id.symbol
+ if sale.pricelist_id.currency_id.position == 'after':
+ res['name'] = _("Advance of %s %s") % (inv_amount, symbol)
+ else:
+ res['name'] = _("Advance of %s %s") % (symbol, inv_amount)
- if res.get('invoice_line_tax_id'):
- res['invoice_line_tax_id'] = [(6, 0, res.get('invoice_line_tax_id'))]
- else:
- res['invoice_line_tax_id'] = False
+ # determine taxes
+ if res.get('invoice_line_tax_id'):
+ res['invoice_line_tax_id'] = [(6, 0, res.get('invoice_line_tax_id'))]
+ else:
+ res['invoice_line_tax_id'] = False
- line_id = obj_lines.create(cr, uid, {
+ # create the invoice
+ inv_line_values = {
+ 'name': res.get('name'),
+ 'account_id': res['account_id'],
+ 'price_unit': inv_amount,
+ 'quantity': wizard.qtty or 1.0,
+ 'discount': False,
+ 'uos_id': res.get('uos_id', False),
+ 'product_id': wizard.product_id.id,
+ 'invoice_line_tax_id': res.get('invoice_line_tax_id'),
+ 'account_analytic_id': sale.project_id.id or False,
+ }
+ inv_values = {
+ 'name': sale.client_order_ref or sale.name,
+ 'origin': sale.name,
+ 'type': 'out_invoice',
+ 'reference': False,
+ 'account_id': sale.partner_id.property_account_receivable.id,
+ 'partner_id': sale.partner_id.id,
+ 'invoice_line': [(0, 0, inv_line_values)],
+ 'currency_id': sale.pricelist_id.currency_id.id,
+ 'comment': '',
+ 'payment_term': sale.payment_term.id,
+ 'fiscal_position': sale.fiscal_position.id or sale.partner_id.property_account_position.id
+ }
+ inv_id = inv_obj.create(cr, uid, inv_values, context=context)
+ inv_obj.button_reset_taxes(cr, uid, [inv_id], context=context)
+ inv_ids.append(inv_id)
+
+ # add the invoice to the sale order's invoices
+ sale.write({'invoice_ids': [(4, inv_id)]})
+
+ # If invoice on picking: add the cost on the SO
+ # If not, the advance will be deduced when generating the final invoice
+ if sale.order_policy == 'picking':
+ vals = {
+ 'order_id': sale.id,
'name': res.get('name'),
- 'account_id': res['account_id'],
- 'price_unit': final_amount,
- 'quantity': sale_adv_obj.qtty or 1.0,
+ 'price_unit': -inv_amount,
+ 'product_uom_qty': wizard.qtty or 1.0,
+ 'product_uos_qty': wizard.qtty or 1.0,
+ 'product_uos': res.get('uos_id', False),
+ 'product_uom': res.get('uom_id', False),
+ 'product_id': wizard.product_id.id or False,
'discount': False,
- 'uos_id': res.get('uos_id', False),
- 'product_id': sale_adv_obj.product_id.id,
- 'invoice_line_tax_id': res.get('invoice_line_tax_id'),
- 'account_analytic_id': sale.project_id.id or False,
- #'note':'',
- })
- create_ids.append(line_id)
- inv = {
- 'name': sale.client_order_ref or sale.name,
- 'origin': sale.name,
- 'type': 'out_invoice',
- 'reference': False,
- 'account_id': sale.partner_id.property_account_receivable.id,
- 'partner_id': sale.partner_id.id,
- 'invoice_line': [(6, 0, create_ids)],
- 'currency_id': sale.pricelist_id.currency_id.id,
- 'comment': '',
- 'payment_term': sale.payment_term.id,
- 'fiscal_position': sale.fiscal_position.id or sale.partner_id.property_account_position.id
+ 'tax_id': res.get('invoice_line_tax_id'),
}
+ self.pool.get('sale.order.line').create(cr, uid, vals, context=context)
- inv_id = inv_obj.create(cr, uid, inv)
- inv_obj.button_reset_taxes(cr, uid, [inv_id], context=context)
-
- for inv in sale.invoice_ids:
- ids_inv.append(inv.id)
- ids_inv.append(inv_id)
- obj_sale.write(cr, uid, [sale.id], {'invoice_ids': [(6, 0, ids_inv)]})
- list_inv.append(inv_id)
- #
- # If invoice on picking: add the cost on the SO
- # If not, the advance will be deduced when generating the final invoice
- #
- if sale.order_policy == 'picking':
- vals = {
- 'order_id': sale.id,
- 'name': res.get('name'),
- 'price_unit': -final_amount,
- 'product_uom_qty': sale_adv_obj.qtty or 1.0,
- 'product_uos_qty': sale_adv_obj.qtty or 1.0,
- 'product_uos': res.get('uos_id', False),
- 'product_uom': res.get('uom_id', False),
- 'product_id': sale_adv_obj.product_id.id or False,
- 'discount': False,
- 'tax_id': res.get('invoice_line_tax_id'),
- }
- self.pool.get('sale.order.line').create(cr, uid, vals, context=context)
-
- context.update({'invoice_id':list_inv})
-
- if context.get('open_invoices'):
- return self.open_invoices( cr, uid, ids, context=context)
+ if context.get('open_invoices', False):
+ return self.open_invoices( cr, uid, ids, inv_ids, context=context)
return {'type': 'ir.actions.act_window_close'}
- def open_invoices(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
- mod_obj = self.pool.get('ir.model.data')
- for advance_pay in self.browse(cr, uid, ids, context=context):
- form_res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
- form_id = form_res and form_res[1] or False
- tree_res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree')
- tree_id = tree_res and tree_res[1] or False
+ def open_invoices(self, cr, uid, ids, invoice_ids, context=None):
+ """ open a view on one of the given invoice_ids """
+ ir_model_data = self.pool.get('ir.model.data')
+ form_res = ir_model_data.get_object_reference(cr, uid, 'account', 'invoice_form')
+ form_id = form_res and form_res[1] or False
+ tree_res = ir_model_data.get_object_reference(cr, uid, 'account', 'invoice_tree')
+ tree_id = tree_res and tree_res[1] or False
return {
'name': _('Advance Invoice'),
'view_type': 'form',
'view_mode': 'form,tree',
'res_model': 'account.invoice',
- 'res_id': int(context['invoice_id'][0]),
+ 'res_id': invoice_ids[0],
'view_id': False,
'views': [(form_id, 'form'), (tree_id, 'tree')],
'context': "{'type': 'out_invoice'}",
'type': 'ir.actions.act_window',
- }
+ }
sale_advance_payment_inv()
diff --git a/addons/sale/wizard/sale_make_invoice_advance.xml b/addons/sale/wizard/sale_make_invoice_advance.xml
index 8d6b78e4f03..28f5dc093cd 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.xml
+++ b/addons/sale/wizard/sale_make_invoice_advance.xml
@@ -7,19 +7,35 @@
form
-
-
-
- or
-
-
-
-
-
+
+
-
-
+
+
+
+
+
+
+
diff --git a/addons/sale_analytic_plans/i18n/nb.po b/addons/sale_analytic_plans/i18n/nb.po
new file mode 100644
index 00000000000..c234f234d72
--- /dev/null
+++ b/addons/sale_analytic_plans/i18n/nb.po
@@ -0,0 +1,28 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:37+0000\n"
+"PO-Revision-Date: 2012-07-22 21:14+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-23 05:17+0000\n"
+"X-Generator: Launchpad (build 15654)\n"
+
+#. module: sale_analytic_plans
+#: field:sale.order.line,analytics_id:0
+msgid "Analytic Distribution"
+msgstr "Analytisk Distribusjon"
+
+#. module: sale_analytic_plans
+#: model:ir.model,name:sale_analytic_plans.model_sale_order_line
+msgid "Sales Order Line"
+msgstr "Salgsordrelinje"
diff --git a/addons/sale_crm/__openerp__.py b/addons/sale_crm/__openerp__.py
index 92b4491d449..b56d866f98b 100644
--- a/addons/sale_crm/__openerp__.py
+++ b/addons/sale_crm/__openerp__.py
@@ -28,12 +28,11 @@ This module adds a shortcut on one or several opportunity cases in the CRM.
===========================================================================
This shortcut allows you to generate a sales order based on the selected case.
-If different cases are open (a list), it generates one sale order by
-case.
+If different cases are open (a list), it generates one sale order by case.
The case is then closed and linked to the generated sales order.
-We suggest you to install this module if you installed both the sale and the
-crm modules.
+We suggest you to install this module, if you installed both the sale and the crm
+modules.
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
diff --git a/addons/sale_crm/sale_crm.py b/addons/sale_crm/sale_crm.py
index 0072730ab4d..b6d5ec25ce1 100644
--- a/addons/sale_crm/sale_crm.py
+++ b/addons/sale_crm/sale_crm.py
@@ -26,7 +26,7 @@ class sale_order(osv.osv):
_columns = {
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
- 'categ_id': fields.many2one('crm.case.categ', 'Category', \
+ 'categ_ids': fields.many2many('crm.case.categ', 'sale_order_category_rel', 'order_id', 'category_id', 'Categories', \
domain="['|',('section_id','=',section_id),('section_id','=',False), ('object_id.model', '=', 'crm.lead')]")
}
diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml
index 0c46f0ae704..d6b7f28d049 100644
--- a/addons/sale_crm/sale_crm_view.xml
+++ b/addons/sale_crm/sale_crm_view.xml
@@ -24,7 +24,7 @@
-
+
diff --git a/addons/sale_crm/wizard/crm_make_sale.py b/addons/sale_crm/wizard/crm_make_sale.py
index 823d8d6ab07..83d7a0a7783 100644
--- a/addons/sale_crm/wizard/crm_make_sale.py
+++ b/addons/sale_crm/wizard/crm_make_sale.py
@@ -86,11 +86,11 @@ class crm_make_sale(osv.osv_memory):
pricelist = partner.property_product_pricelist.id
if False in partner_addr.values():
raise osv.except_osv(_('Data Insufficient!'), _('Customer has no addresses defined!'))
-
+
vals = {
'origin': _('Opportunity: %s') % str(case.id),
'section_id': case.section_id and case.section_id.id or False,
- 'categ_id': case.categ_id and case.categ_id.id or False,
+ 'categ_ids': [(6, 0, [categ_id.id for categ_id in case.categ_ids])],
'shop_id': make.shop_id.id,
'partner_id': partner.id,
'pricelist_id': pricelist,
diff --git a/addons/sale_journal/__openerp__.py b/addons/sale_journal/__openerp__.py
index c104ae19d97..11408161bf4 100644
--- a/addons/sale_journal/__openerp__.py
+++ b/addons/sale_journal/__openerp__.py
@@ -27,8 +27,7 @@
The sales journal modules allows you to categorise your sales and deliveries (picking lists) between different journals.
========================================================================================================================
-This module is very helpful for bigger companies that
-works by departments.
+This module is very helpful for bigger companies that works by departments.
You can use journal for different purposes, some examples:
* isolate sales of different departments
@@ -37,12 +36,13 @@ You can use journal for different purposes, some examples:
Journals have a responsible and evolves between different status:
* draft, open, cancel, done.
-Batch operations can be processed on the different journals to
-confirm all sales at once, to validate or invoice packing, ...
+Batch operations can be processed on the different journals to confirm all sales
+at once, to validate or invoice packing.
-It also supports batch invoicing methods that can be configured by partners and sales orders, examples:
- * daily invoicing,
- * monthly invoicing, ...
+It also supports batch invoicing methods that can be configured by partners and
+sales orders, examples:
+ * daily invoicing
+ * monthly invoicing
Some statistics by journals are provided.
""",
diff --git a/addons/sale_margin/__openerp__.py b/addons/sale_margin/__openerp__.py
index 623f442578a..936f5a6b350 100644
--- a/addons/sale_margin/__openerp__.py
+++ b/addons/sale_margin/__openerp__.py
@@ -26,7 +26,8 @@
This module adds the 'Margin' on sales order.
=============================================
-This gives the profitability by calculating the difference between the Unit Price and Cost Price.
+This gives the profitability by calculating the difference between the Unit
+Price and Cost Price.
""",
"author":"OpenERP SA",
"images":["images/sale_margin.jpeg"],
diff --git a/addons/sale_margin/i18n/nb.po b/addons/sale_margin/i18n/nb.po
new file mode 100644
index 00000000000..7db7e12a698
--- /dev/null
+++ b/addons/sale_margin/i18n/nb.po
@@ -0,0 +1,52 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:37+0000\n"
+"PO-Revision-Date: 2012-07-22 21:27+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-23 05:17+0000\n"
+"X-Generator: Launchpad (build 15654)\n"
+
+#. module: sale_margin
+#: sql_constraint:sale.order:0
+msgid "Order Reference must be unique per Company!"
+msgstr "Ordrereferanse må være unik pr. firma!"
+
+#. module: sale_margin
+#: field:sale.order.line,purchase_price:0
+msgid "Cost Price"
+msgstr "Kostpris"
+
+#. module: sale_margin
+#: model:ir.model,name:sale_margin.model_sale_order
+msgid "Sales Order"
+msgstr "Salgsordre"
+
+#. module: sale_margin
+#: help:sale.order,margin:0
+msgid ""
+"It gives profitability by calculating the difference between the Unit Price "
+"and Cost Price."
+msgstr ""
+"Fortjeneste oppnås ved å beregne differansen mellom enhetspris og kostpris"
+
+#. module: sale_margin
+#: field:sale.order,margin:0
+#: field:sale.order.line,margin:0
+msgid "Margin"
+msgstr "Margin"
+
+#. module: sale_margin
+#: model:ir.model,name:sale_margin.model_sale_order_line
+msgid "Sales Order Line"
+msgstr "Salgsordrelinje"
diff --git a/addons/sale_margin/sale_margin_view.xml b/addons/sale_margin/sale_margin_view.xml
index 4696eec6eb2..30c2a0dea14 100644
--- a/addons/sale_margin/sale_margin_view.xml
+++ b/addons/sale_margin/sale_margin_view.xml
@@ -8,8 +8,13 @@
sale.order
-
-
+
+
+
+
+
+
+
diff --git a/addons/sale_mrp/__openerp__.py b/addons/sale_mrp/__openerp__.py
index 2d7ed96cca4..d5b3d6b7849 100644
--- a/addons/sale_mrp/__openerp__.py
+++ b/addons/sale_mrp/__openerp__.py
@@ -28,9 +28,8 @@
This module provides facility to the user to install mrp and sales modulesat a time.
====================================================================================
-It is basically used when we want to keep track of production
-orders generated from sales order.
-It adds sales name and sales Reference on production order.
+It is basically used when we want to keep track of production orders generated
+from sales order. It adds sales name and sales Reference on production order.
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
diff --git a/addons/sale_mrp/i18n/nb.po b/addons/sale_mrp/i18n/nb.po
new file mode 100644
index 00000000000..4b6589b24cc
--- /dev/null
+++ b/addons/sale_mrp/i18n/nb.po
@@ -0,0 +1,53 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:37+0000\n"
+"PO-Revision-Date: 2012-07-22 20:19+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-23 05:17+0000\n"
+"X-Generator: Launchpad (build 15654)\n"
+
+#. module: sale_mrp
+#: help:mrp.production,sale_ref:0
+msgid "Indicate the Customer Reference from sales order."
+msgstr "Viser kundereferansen fra salgsordren"
+
+#. module: sale_mrp
+#: field:mrp.production,sale_ref:0
+msgid "Sales Reference"
+msgstr "Salgsreferanse"
+
+#. module: sale_mrp
+#: model:ir.model,name:sale_mrp.model_mrp_production
+msgid "Manufacturing Order"
+msgstr "Produksjonsordre"
+
+#. module: sale_mrp
+#: field:mrp.production,sale_name:0
+msgid "Sales Name"
+msgstr "Salgsnavn"
+
+#. module: sale_mrp
+#: sql_constraint:mrp.production:0
+msgid "Reference must be unique per Company!"
+msgstr "Referanse må være unik pr firma!"
+
+#. module: sale_mrp
+#: constraint:mrp.production:0
+msgid "Order quantity cannot be negative or zero!"
+msgstr "Ordrekvantum kan ikke være negativt eller null!"
+
+#. module: sale_mrp
+#: help:mrp.production,sale_name:0
+msgid "Indicate the name of sales order."
+msgstr "Angi navnet på salgsordren."
diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/sale_mrp.py
index 840c33770a7..7397edbec2b 100644
--- a/addons/sale_mrp/sale_mrp.py
+++ b/addons/sale_mrp/sale_mrp.py
@@ -70,8 +70,8 @@ class mrp_production(osv.osv):
return res
_columns = {
- 'sale_name': fields.function(_ref_calc, multi='sale_name', type='char', string='Sales Name', help='Indicate the name of sales order.'),
- 'sale_ref': fields.function(_ref_calc, multi='sale_name', type='char', string='Sales Reference', help='Indicate the Customer Reference from sales order.'),
+ 'sale_name': fields.function(_ref_calc, multi='sale_name', type='char', string='Sale Name', help='Indicate the name of sales order.'),
+ 'sale_ref': fields.function(_ref_calc, multi='sale_name', type='char', string='Sale Reference', help='Indicate the Customer Reference from sales order.'),
}
mrp_production()
diff --git a/addons/sale_mrp/sale_mrp_view.xml b/addons/sale_mrp/sale_mrp_view.xml
index fbf2e7a9327..882663680c5 100644
--- a/addons/sale_mrp/sale_mrp_view.xml
+++ b/addons/sale_mrp/sale_mrp_view.xml
@@ -8,10 +8,12 @@
form
-
-
+
+
+
+
diff --git a/addons/sale_order_dates/i18n/nb.po b/addons/sale_order_dates/i18n/nb.po
new file mode 100644
index 00000000000..9d4322c8535
--- /dev/null
+++ b/addons/sale_order_dates/i18n/nb.po
@@ -0,0 +1,58 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:37+0000\n"
+"PO-Revision-Date: 2012-07-23 10:58+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-07-24 04:52+0000\n"
+"X-Generator: Launchpad (build 15668)\n"
+
+#. module: sale_order_dates
+#: sql_constraint:sale.order:0
+msgid "Order Reference must be unique per Company!"
+msgstr "Ordrereferanse må være unik pr. firma!"
+
+#. module: sale_order_dates
+#: help:sale.order,requested_date:0
+msgid "Date on which customer has requested for sales."
+msgstr "Dato som kunden har ønsket for salg."
+
+#. module: sale_order_dates
+#: field:sale.order,commitment_date:0
+msgid "Commitment Date"
+msgstr "Bekreftet dato"
+
+#. module: sale_order_dates
+#: field:sale.order,effective_date:0
+msgid "Effective Date"
+msgstr "Behandlingsdato"
+
+#. module: sale_order_dates
+#: help:sale.order,effective_date:0
+msgid "Date on which picking is created."
+msgstr "Dato hvor plukkingen er gjort."
+
+#. module: sale_order_dates
+#: field:sale.order,requested_date:0
+msgid "Requested Date"
+msgstr "Ønsket dato"
+
+#. module: sale_order_dates
+#: model:ir.model,name:sale_order_dates.model_sale_order
+msgid "Sales Order"
+msgstr "Salgsordre"
+
+#. module: sale_order_dates
+#: help:sale.order,commitment_date:0
+msgid "Date on which delivery of products is to be made."
+msgstr "Dato hvor levering av varer skal skje"
diff --git a/addons/share/__openerp__.py b/addons/share/__openerp__.py
index 8b997e2357e..f79024fa1c7 100644
--- a/addons/share/__openerp__.py
+++ b/addons/share/__openerp__.py
@@ -32,15 +32,14 @@ This module adds generic sharing tools to your current OpenERP database.
========================================================================
It specifically adds a 'share' button that is available in the Web client to
-share any kind of OpenERP data with colleagues, customers, friends, etc.
+share any kind of OpenERP data with colleagues, customers, friends.
The system will work by creating new users and groups on the fly, and by
-combining the appropriate access rights and ir.rules to ensure that the
-shared users only have access to the data that has been shared with them.
+combining the appropriate access rights and ir.rules to ensure that the shared
+users only have access to the data that has been shared with them.
This is extremely useful for collaborative work, knowledge sharing,
-synchronization with other companies, etc.
-
+synchronization with other companies.
""",
'website': 'http://www.openerp.com',
'demo_xml': ['share_demo.xml'],
diff --git a/addons/share/wizard/share_wizard.py b/addons/share/wizard/share_wizard.py
index 41871480f45..23245e12a2d 100644
--- a/addons/share/wizard/share_wizard.py
+++ b/addons/share/wizard/share_wizard.py
@@ -274,7 +274,6 @@ class share_wizard(osv.TransientModel):
'name': new_login,
'groups_id': [(6,0,[group_id])],
'share': True,
- 'menu_tips' : False,
'company_id': current_user.company_id.id
}, context)
new_line = { 'user_id': user_id,
diff --git a/addons/signup/__init__.py b/addons/signup/__init__.py
new file mode 100644
index 00000000000..d70c1894925
--- /dev/null
+++ b/addons/signup/__init__.py
@@ -0,0 +1,2 @@
+import res_config
+import signup
diff --git a/addons/signup/__openerp__.py b/addons/signup/__openerp__.py
new file mode 100644
index 00000000000..b899d5bc434
--- /dev/null
+++ b/addons/signup/__openerp__.py
@@ -0,0 +1,20 @@
+{
+ 'name': 'Signup',
+ 'description': 'Allow users to sign up',
+ 'author': 'OpenERP SA',
+ 'version': '1.0',
+ 'category': 'Tools',
+ 'website': 'http://www.openerp.com',
+ 'installable': True,
+ 'depends': ['anonymous', 'base_setup'],
+ 'data': [
+ 'res_config.xml',
+ 'signup.xml',
+ ],
+ 'js': [
+ 'static/src/js/signup.js',
+ ],
+ 'qweb': [
+ 'static/src/xml/signup.xml',
+ ],
+}
diff --git a/addons/signup/res_config.py b/addons/signup/res_config.py
new file mode 100644
index 00000000000..53f0513b234
--- /dev/null
+++ b/addons/signup/res_config.py
@@ -0,0 +1,20 @@
+from openerp.osv import osv, fields
+
+class base_config_settings(osv.TransientModel):
+ _inherit = 'base.config.settings'
+
+ _columns = {
+ 'signup_template_user_id': fields.many2one('res.users', 'Template user for signup')
+ }
+
+ def get_default_signup(self, cr, uid, fields, context=None):
+ icp = self.pool.get('ir.config_parameter')
+ return {
+ 'signup_template_user_id': icp.get_param(cr, uid, 'signup.template_user_id', 0) or False
+ }
+
+ def set_signup(self, cr, uid, ids, context=None):
+ config = self.browse(cr, uid, ids[0], context=context)
+ icp = self.pool.get('ir.config_parameter')
+ icp.set_param(cr, uid, 'signup.template_user_id', config.signup_user_template_id.id)
+
diff --git a/addons/signup/res_config.xml b/addons/signup/res_config.xml
new file mode 100644
index 00000000000..d13f7002d17
--- /dev/null
+++ b/addons/signup/res_config.xml
@@ -0,0 +1,16 @@
+
+
+
+ base.config.settings.signup
+ base.config.settings
+
+ form
+
+
+
+
+
+
+
+
+
diff --git a/addons/signup/signup.py b/addons/signup/signup.py
new file mode 100644
index 00000000000..3819a63da85
--- /dev/null
+++ b/addons/signup/signup.py
@@ -0,0 +1,57 @@
+from openerp.osv import osv, fields
+
+class res_users(osv.Model):
+ _inherit = 'res.users'
+
+ _sql_constraints = [
+ ('email_uniq', 'UNIQUE (user_email)', 'You can not have two users with the same email!')
+ ]
+
+class signup_signup(osv.TransientModel):
+ _name = 'signup.signup'
+ _columns = {
+ 'name': fields.char('Name', size=64),
+ 'email': fields.char('Email', size=64),
+ 'password': fields.char('Password', size=64),
+ 'password_confirmation': fields.char('Confirm Password', size=64),
+ 'state': fields.selection([(x, x) for x in 'draft done missmatch'.split()], required=True),
+ }
+ _defaults = {
+ 'state': 'draft',
+ }
+
+ def create(self, cr, uid, values, context=None):
+ # NOTE here, invalid values raises exceptions to avoid storing
+ # sensitive data into the database (which then are available to anyone)
+ if values['password'] != values['password_confirmation']:
+ raise osv.except_osv('Error', 'Passwords missmatch')
+
+ new_user = {
+ 'name': values['name'],
+ 'login': values['email'],
+ 'user_email': values['email'],
+ 'password': values['password'],
+ 'active': True,
+ }
+
+ user_template_id = self.pool.get('ir.config_parameter').get_param(cr, uid, 'signup.user_template_id', 0)
+ if user_template_id:
+ self.pool.get('res.users').copy(cr, 1, user_template_id, new_user, context=context)
+ else:
+ self.pool.get('res.users').create(cr, 1, new_user, context=context)
+
+ # Dont store the password
+ values = {'state': 'done'}
+ return super(signup_signup, self).create(cr, uid, values, context)
+
+ def signup(self, cr, uid, ids, context=None):
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'login',
+ }
+
+ def onchange_pw(self, cr, uid, ids, pw, cpw, context=None):
+ if pw != cpw:
+ return {'value': {'state': 'missmatch'}}
+ return {'value': {'state': 'draft'}}
+
diff --git a/addons/signup/signup.xml b/addons/signup/signup.xml
new file mode 100644
index 00000000000..3dacf85feb4
--- /dev/null
+++ b/addons/signup/signup.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ signup.signup.form
+ signup.signup
+ form
+
+
+
+
+
+
+
+
+
+
Passwords missmatch
+
+
+
+
+
+
+
You can now login.
+
+
+
+
+
+
+
+ signup.signup
+ ir.actions.act_window
+ signup.signup
+ form
+ form
+ new
+
+
+
+
diff --git a/addons/signup/static/src/js/signup.js b/addons/signup/static/src/js/signup.js
new file mode 100644
index 00000000000..aa469512b5b
--- /dev/null
+++ b/addons/signup/static/src/js/signup.js
@@ -0,0 +1,21 @@
+openerp.signup = function(instance) {
+
+ instance.web.UserMenu.include({
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+ this.$element.find('a.oe_topbar_signup').click(function() {
+ var p = self.getParent();
+ var am = p.action_manager;
+ am.do_action({
+ type:'ir.actions.act_window',
+ res_model: 'signup.signup',
+ views: [[false, 'form']],
+ target: 'new',
+ name: 'Sign Up'
+ });
+ });
+ }
+ });
+
+};
diff --git a/addons/signup/static/src/xml/signup.xml b/addons/signup/static/src/xml/signup.xml
new file mode 100644
index 00000000000..c6c7f555ecd
--- /dev/null
+++ b/addons/signup/static/src/xml/signup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+ Sign Up
+
+
+
+
diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py
index 50bcb23df7b..d061fb4b033 100644
--- a/addons/stock/__openerp__.py
+++ b/addons/stock/__openerp__.py
@@ -23,25 +23,27 @@
"name" : "Warehouse Management",
"version" : "1.1",
"author" : "OpenERP SA",
+ "summary": "Incoming Shipments, Deliveries, Inventory, Stock",
"description" : """
OpenERP Inventory Management module can manage multi-warehouses, multi and structured stock locations.
======================================================================================================
Thanks to the double entry management, the inventory controlling is powerful and flexible:
* Moves history and planning,
- * Different inventory methods (FIFO, LIFO, ...)
* Stock valuation (standard or average price, ...)
* Robustness faced with Inventory differences
* Automatic reordering rules (stock level, JIT, ...)
* Bar code supported
* Rapid detection of mistakes through double entry system
* Traceability (upstream/downstream, production lots, serial number, ...)
- * Dashboard for warehouse that includes:
- * Procurement in exception
- * List of Incoming Products
- * List of Outgoing Products
- * Graph : Products to receive in delay (date < = today)
- * Graph : Products to send in delay (date < = today)
+
+Dashboard for warehouse that includes:
+--------------------------------------
+ * Procurement in exception
+ * List of Incoming Products
+ * List of Outgoing Products
+ * Graph : Products to receive in delay (date <= today)
+ * Graph : Products to send in delay (date <= today)
""",
"website" : "http://www.openerp.com",
"images" : ["images/stock_forecast_report.png", "images/delivery_orders.jpeg", "images/inventory_analysis.jpeg","images/location.jpeg","images/moves_analysis.jpeg","images/physical_inventories.jpeg","images/warehouse_dashboard.jpeg"],
diff --git a/addons/stock/res_config_view.xml b/addons/stock/res_config_view.xml
index f6638f22002..0503ec544b1 100644
--- a/addons/stock/res_config_view.xml
+++ b/addons/stock/res_config_view.xml
@@ -13,36 +13,30 @@
or
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
+
-
-
-
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index a279b8bea6c..6d8f6bb6fec 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -1726,10 +1726,15 @@ class stock_move(osv.osv):
if location_xml_id:
location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id)
return location_id
+
+ def _default_destination_address(self, cr, uid, context=None):
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ return user.company_id.partner_id.id
_defaults = {
'location_id': _default_location_source,
'location_dest_id': _default_location_destination,
+ 'partner_id': _default_destination_address,
'state': 'draft',
'priority': '1',
'product_qty': 1.0,
diff --git a/addons/stock/stock_data.xml b/addons/stock/stock_data.xml
index 3e3ffcb83f5..69bb610ae99 100644
--- a/addons/stock/stock_data.xml
+++ b/addons/stock/stock_data.xml
@@ -1,6 +1,17 @@
+
+
+
+
+ Module Warehouse Management has been installed
+ Manage your product inventoy and stock locations. You can
+ control your stock moves history and planning, perform stock
+ valuation, and trace product lots upstream and downstream
+ (based on serial numbers.)
+
+ Stock Journal Sequence
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index 432b3ce365b..d4acd4906ad 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -2,15 +2,17 @@
-
+
+
+
+ parent="stock.menu_stock_product" sequence="0"/>
@@ -20,19 +22,19 @@
parent="stock.menu_stock_configuration" sequence="45" groups="base.group_no_one"/>
+ parent="stock.menu_product_in_config_stock" sequence="0"/>
-
-
-
+
+ stock.inventory.line.tree
@@ -116,30 +118,26 @@
-
-
+
+
-
-
-
-
+
+
-
+ -
+
-
-
-
-
+
@@ -150,19 +148,27 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
@@ -200,7 +206,7 @@
stock.inventoryform
-
+ Periodical Inventories are used to count the number of products available per location. You can use it once a year when you do the general inventory or whenever you need it, to correct the current stock level of a product.
@@ -271,9 +277,9 @@
child_ids
-
-
-
+
+
+
@@ -287,7 +293,7 @@
-
+
@@ -331,18 +337,25 @@
form
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
-
-
@@ -376,7 +389,7 @@
tree
-
+
@@ -397,7 +410,7 @@
-
+
@@ -415,7 +428,7 @@
stock.production.lotform
-
+ {}This is the list of all the production lots (serial numbers) you recorded. When you select a lot, you can get the upstream or downstream traceability of the products contained in lot. By default, the list is filtred on the serial numbers that are available in your warehouse but you can uncheck the 'Available' button to get all the lots you produced, received or delivered to customers.
@@ -433,8 +446,8 @@
move_history_ids
-
-
+
+
@@ -457,8 +470,8 @@
move_history_ids2
-
-
+
+
@@ -558,11 +571,11 @@
+ string="Internal" domain="[('usage', '=', 'internal')]" help="Internal Locations"/>
+ string="Customer" domain="[('usage', '=', 'customer')]" help="Customer Locations"/>
+ string="Supplier" domain="[('usage', '=', 'supplier')]" help="Supplier Locations"/>
@@ -726,7 +739,7 @@
-
+
@@ -734,7 +747,7 @@
-
-
-
stock.move.incoming.search2stock.move
@@ -1552,7 +1495,7 @@
form
-
+
diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py
index 9a00a6d62ce..0cc97e10298 100644
--- a/addons/stock/wizard/stock_change_product_qty.py
+++ b/addons/stock/wizard/stock_change_product_qty.py
@@ -62,6 +62,9 @@ class stock_change_product_qty(osv.osv_memory):
res.update({'new_quantity': 1})
if 'product_id' in fields:
res.update({'product_id': product_id})
+ if 'location_id' in fields:
+ location_id = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
+ res.update({'location_id': location_id and location_id.id or False})
return res
def change_product_qty(self, cr, uid, ids, context=None):
diff --git a/addons/stock/wizard/stock_change_product_qty_view.xml b/addons/stock/wizard/stock_change_product_qty_view.xml
index e360f5aa025..6713194f90b 100644
--- a/addons/stock/wizard/stock_change_product_qty_view.xml
+++ b/addons/stock/wizard/stock_change_product_qty_view.xml
@@ -10,8 +10,8 @@
-
-
+
+
diff --git a/addons/stock_planning/wizard/stock_planning_create_periods_view.xml b/addons/stock_planning/wizard/stock_planning_create_periods_view.xml
index e65f378a638..72d70ae5203 100644
--- a/addons/stock_planning/wizard/stock_planning_create_periods_view.xml
+++ b/addons/stock_planning/wizard/stock_planning_create_periods_view.xml
@@ -10,8 +10,12 @@
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -268,7 +271,7 @@
-
+ survey_searchsurveysearch
@@ -294,7 +297,7 @@
-
+
event.survey.kanbansurvey
@@ -749,7 +752,7 @@
-
+ survey_question_searchsurvey.questionsearch
diff --git a/addons/users_ldap/__openerp__.py b/addons/users_ldap/__openerp__.py
index e6cd59e8131..ba153328af1 100644
--- a/addons/users_ldap/__openerp__.py
+++ b/addons/users_ldap/__openerp__.py
@@ -27,90 +27,76 @@
"description": """
Adds support for authentication by LDAP server.
===============================================
-This module allows users to login with their LDAP username and
-password, and will automatically create OpenERP users for them
-on the fly.
+This module allows users to login with their LDAP username and password, and
+will automatically create OpenERP users for them on the fly.
-**Note**: This module only work on servers who have Python's
-``ldap`` module installed.
+Note: This module only work on servers who have Python's ``ldap`` module installed.
-Configuration
-+++++++++++++
-After installing this module, you need to configure the LDAP
-parameters in the Configuration tab of the Company details.
-Different companies may have different LDAP servers, as long
-as they have unique usernames (usernames need to be unique in
-OpenERP, even across multiple companies).
+Configuration:
+--------------
+After installing this module, you need to configure the LDAP parameters in the
+Configuration tab of the Company details. Different companies may have different
+LDAP servers, as long as they have unique usernames (usernames need to be unique
+in OpenERP, even across multiple companies).
-Anonymous LDAP binding is also supported (for LDAP servers
-that allow it), by simpling keeping the LDAP user and password
-empty in the LDAP configuration. This does **not** allow
-anonymous authentication for users, it is only for the master
-LDAP account that is used to verify if a user exists before
-attempting to authenticate it.
+Anonymous LDAP binding is also supported (for LDAP servers that allow it), by
+simply keeping the LDAP user and password empty in the LDAP configuration.
+This does not allow anonymous authentication for users, it is only for the master
+LDAP account that is used to verify if a user exists before attempting to
+authenticate it.
-Securing the connection with STARTTLS is available for LDAP
-servers supporting it, by enabling the TLS option in the LDAP
-configuration.
+Securing the connection with STARTTLS is available for LDAP servers supporting
+it, by enabling the TLS option in the LDAP configuration.
-For further options configuring the LDAP settings, refer to the
-ldap.conf manpage: manpage:`ldap.conf(5)`.
+For further options configuring the LDAP settings, refer to the ldap.conf
+manpage: manpage:`ldap.conf(5)`.
-Security Considerations
-+++++++++++++++++++++++
-Users' LDAP passwords are never stored in the OpenERP database,
-the LDAP server is queried whenever a user needs to be
-authenticated. No duplication of the password occurs, and
-passwords are managed in one place only.
+Security Considerations:
+------------------------
+Users' LDAP passwords are never stored in the OpenERP database, the LDAP server
+is queried whenever a user needs to be authenticated. No duplication of the
+password occurs, and passwords are managed in one place only.
-OpenERP does not manage password changes in the LDAP, so
-any change of password should be conducted by other means
-in the LDAP directory directly (for LDAP users).
+OpenERP does not manage password changes in the LDAP, so any change of password
+should be conducted by other means in the LDAP directory directly (for LDAP users).
-It is also possible to have local OpenERP users in the
-database along with LDAP-authenticated users (the Administrator
-account is one obvious example).
+It is also possible to have local OpenERP users in the database along with
+LDAP-authenticated users (the Administrator account is one obvious example).
Here is how it works:
- * The system first attempts to authenticate users against
- the local OpenERP database ;
- * if this authentication fails (for example because the
- user has no local password), the system then attempts
- to authenticate against LDAP ;
+ * The system first attempts to authenticate users against the local OpenERP
+ database;
+ * if this authentication fails (for example because the user has no local
+ password), the system then attempts to authenticate against LDAP;
-As LDAP users have blank passwords by default in the local
-OpenERP database (which means no access), the first step
-always fails and the LDAP server is queried to do the
-authentication.
+As LDAP users have blank passwords by default in the local OpenERP database
+(which means no access), the first step always fails and the LDAP server is
+queried to do the authentication.
-Enabling STARTTLS ensures that the authentication query to the
-LDAP server is encrypted.
+Enabling STARTTLS ensures that the authentication query to the LDAP server is
+encrypted.
-User Template
-+++++++++++++
-In the LDAP configuration on the Company form, it is possible to
-select a *User Template*. If set, this user will be used as
-template to create the local users whenever someone authenticates
-for the first time via LDAP authentication.
-This allows pre-setting the default groups and menus of the
-first-time users.
+User Template:
+--------------
+In the LDAP configuration on the Company form, it is possible to select a *User
+Template*. If set, this user will be used as template to create the local users
+whenever someone authenticates for the first time via LDAP authentication. This
+allows pre-setting the default groups and menus of the first-time users.
-**Warning**: if you set a password for the user template,
-this password will be assigned as local password for each new
-LDAP user, effectively setting a *master password* for these
-users (until manually changed). You usually do not want this.
-One easy way to setup a template user is to login once with
-a valid LDAP user, let OpenERP create a blank local user with the
-same login (and a blank password), then rename this new user
-to a username that does not exist in LDAP, and setup its
-groups the way you want.
-
-Interaction with base_crypt
-+++++++++++++++++++++++++++
-The base_crypt module is not compatible with this module, and
-will disable LDAP authentication if installed at the same time.
+Warning: if you set a password for the user template, this password will be
+ assigned as local password for each new LDAP user, effectively setting
+ a *master password* for these users (until manually changed). You
+ usually do not want this. One easy way to setup a template user is to
+ login once with a valid LDAP user, let OpenERP create a blank local
+ user with the same login (and a blank password), then rename this new
+ user to a username that does not exist in LDAP, and setup its groups
+ the way you want.
+Interaction with base_crypt:
+----------------------------
+The base_crypt module is not compatible with this module, and will disable LDAP
+authentication if installed at the same time.
""",
diff --git a/addons/users_ldap/users_ldap_view.xml b/addons/users_ldap/users_ldap_view.xml
index 40d01f43bf3..b87800aa8c8 100644
--- a/addons/users_ldap/users_ldap_view.xml
+++ b/addons/users_ldap/users_ldap_view.xml
@@ -7,30 +7,32 @@
form
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/addons/web_livechat/i18n/ar.po b/addons/web_livechat/i18n/ar.po
deleted file mode 100644
index 0de2cbea2a3..00000000000
--- a/addons/web_livechat/i18n/ar.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Arabic translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-01-07 23:53+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Arabic \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "ذلك العقد قد تمّ تسجيله في النظام سابقاً."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "التحدث مع الدعم الفني"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "تمكين الدعم الفني المباشر للحاصلين علي عقد دعم فني"
diff --git a/addons/web_livechat/i18n/bg.po b/addons/web_livechat/i18n/bg.po
deleted file mode 100644
index 7b7ac0bdc15..00000000000
--- a/addons/web_livechat/i18n/bg.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Bulgarian translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-03-01 14:52+0000\n"
-"Last-Translator: Dimitar Markov \n"
-"Language-Team: Bulgarian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr ""
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Поддръжка чат на живо"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Разрешете live chat поддръжка ако имате договор за поддръжка"
diff --git a/addons/web_livechat/i18n/ca.po b/addons/web_livechat/i18n/ca.po
deleted file mode 100644
index 1af1c22a74a..00000000000
--- a/addons/web_livechat/i18n/ca.po
+++ /dev/null
@@ -1,35 +0,0 @@
-# Catalan translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-03-05 23:39+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Catalan \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Aquest contracte ja està registrat al sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "editor_garantia.contracte"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Xat d'assistència"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Permet suport per xat per a aquells que ja tenen contracte de manteniment"
diff --git a/addons/web_livechat/i18n/cs.po b/addons/web_livechat/i18n/cs.po
deleted file mode 100644
index 9e3a2b95fd2..00000000000
--- a/addons/web_livechat/i18n/cs.po
+++ /dev/null
@@ -1,28 +0,0 @@
-# Czech translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-04-06 04:48+0000\n"
-"Last-Translator: Jiří Hajda \n"
-"Language-Team: Czech \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Tato smlouva je již v systému registrována."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
diff --git a/addons/web_livechat/i18n/da.po b/addons/web_livechat/i18n/da.po
deleted file mode 100644
index d982f1ff2fa..00000000000
--- a/addons/web_livechat/i18n/da.po
+++ /dev/null
@@ -1,28 +0,0 @@
-# Danish translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-01-27 06:45+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Danish \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr ""
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr ""
diff --git a/addons/web_livechat/i18n/de.po b/addons/web_livechat/i18n/de.po
deleted file mode 100644
index 5a6daa96fc8..00000000000
--- a/addons/web_livechat/i18n/de.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# German translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-02-21 20:28+0000\n"
-"Last-Translator: Ferdinand @ Camptocamp \n"
-"Language-Team: German \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Der Wartungsvertrag wurde bereits im System registriert."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Live Chat Unterstützung"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Live Chat für Vetragsinhaber aktivieren"
diff --git a/addons/web_livechat/i18n/es.po b/addons/web_livechat/i18n/es.po
deleted file mode 100644
index 6050532feb2..00000000000
--- a/addons/web_livechat/i18n/es.po
+++ /dev/null
@@ -1,36 +0,0 @@
-# Spanish translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-02-15 15:37+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Spanish \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Ese contrato ya está registrado en el sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "editor_garantía.contrato"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Chat de asistencia"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Permite soporte por chat para aquellos que ya tienen contrato de "
-#~ "mantenimiento"
diff --git a/addons/web_livechat/i18n/es_CR.po b/addons/web_livechat/i18n/es_CR.po
deleted file mode 100644
index a3b28d1a535..00000000000
--- a/addons/web_livechat/i18n/es_CR.po
+++ /dev/null
@@ -1,38 +0,0 @@
-# Spanish translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-02-18 23:24+0000\n"
-"Last-Translator: Carlos Vásquez (CLEARCORP) "
-"\n"
-"Language-Team: Spanish \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-"Language: es\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Ese contrato ya está registrado en el sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "editor_garantía.contrato"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Chat de asistencia"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Permite soporte por chat para aquellos que ya tienen contrato de "
-#~ "mantenimiento"
diff --git a/addons/web_livechat/i18n/fi.po b/addons/web_livechat/i18n/fi.po
deleted file mode 100644
index e83df90eba8..00000000000
--- a/addons/web_livechat/i18n/fi.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Finnish translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-06-08 10:30+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Finnish \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Sopimus on jo rekisteröity järjestelmään."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr ""
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Chatin tuki"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Aloita chatti tuki ylläpitosopimuksen toimittajan kanssa"
diff --git a/addons/web_livechat/i18n/fr.po b/addons/web_livechat/i18n/fr.po
deleted file mode 100644
index b40a278fa23..00000000000
--- a/addons/web_livechat/i18n/fr.po
+++ /dev/null
@@ -1,36 +0,0 @@
-# French translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-02-26 09:39+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: French \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Ce contrat est déjà enregistré dans le système."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Support par messagerie instantanée"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Valider le support par messagerie instantanée pour les détenteurs d'un "
-#~ "contrat de maintenance"
diff --git a/addons/web_livechat/i18n/gl.po b/addons/web_livechat/i18n/gl.po
deleted file mode 100644
index 57027697505..00000000000
--- a/addons/web_livechat/i18n/gl.po
+++ /dev/null
@@ -1,35 +0,0 @@
-# Galician translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-03-05 01:41+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Galician \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "O contrato xa está rexistrado no sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "editor_garantía.contrato"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Chat de asistencia"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Permitir soporte por chat para aqueles que xa teñan contrato de mantemento"
diff --git a/addons/web_livechat/i18n/hr.po b/addons/web_livechat/i18n/hr.po
deleted file mode 100644
index a8d29394b6a..00000000000
--- a/addons/web_livechat/i18n/hr.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Croatian translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-01-28 21:59+0000\n"
-"Last-Translator: Goran Kliska \n"
-"Language-Team: Croatian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Navedeni ugovor je već registriran u sustavu."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Omogući podršku za Live Chat za one koji imaju ugovor o održavanju"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Podrška za Live Chat"
diff --git a/addons/web_livechat/i18n/it.po b/addons/web_livechat/i18n/it.po
deleted file mode 100644
index 5385120f698..00000000000
--- a/addons/web_livechat/i18n/it.po
+++ /dev/null
@@ -1,35 +0,0 @@
-# Italian translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-02-15 15:37+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Italian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Il contratto è già registrato nel sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Supporto live chat"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Abilita il supporto live chat per chi ha un contratto di manutenzione"
diff --git a/addons/web_livechat/i18n/ja.po b/addons/web_livechat/i18n/ja.po
deleted file mode 100644
index ca047fc4dfa..00000000000
--- a/addons/web_livechat/i18n/ja.po
+++ /dev/null
@@ -1,28 +0,0 @@
-# Japanese translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-06-16 21:21+0000\n"
-"Last-Translator: Akira Hiyama \n"
-"Language-Team: Japanese \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "この契約は既にシステムに登録されています。"
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr ""
diff --git a/addons/web_livechat/i18n/mn.po b/addons/web_livechat/i18n/mn.po
deleted file mode 100644
index 9909272a48b..00000000000
--- a/addons/web_livechat/i18n/mn.po
+++ /dev/null
@@ -1,28 +0,0 @@
-# Mongolian translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-07-08 18:51+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Mongolian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Энэ гэрээг системд өмнө нь бүртгэгдсэн байна."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
diff --git a/addons/web_livechat/i18n/nl.po b/addons/web_livechat/i18n/nl.po
deleted file mode 100644
index ebb638a88ee..00000000000
--- a/addons/web_livechat/i18n/nl.po
+++ /dev/null
@@ -1,35 +0,0 @@
-# Dutch translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-02-21 10:22+0000\n"
-"Last-Translator: Douwe Wullink (Dypalio) \n"
-"Language-Team: Dutch \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Dat contract is al geregisteerd in het systeem."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Live Chat Support"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Live chat support aanzetten voor diegenen die een onderhoudscontract hebben"
diff --git a/addons/web_livechat/i18n/oc.po b/addons/web_livechat/i18n/oc.po
deleted file mode 100644
index edaa45c213d..00000000000
--- a/addons/web_livechat/i18n/oc.po
+++ /dev/null
@@ -1,36 +0,0 @@
-# Occitan (post 1500) translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-11-20 09:29+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Occitan (post 1500) \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Aqueste contracte es ja enregistrat dins lo sistèma."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Supòrt per messatjariá instantanèa"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Validar lo supòrt per messatjariá instantanèa pels detentors d'un contracte "
-#~ "de mantenença"
diff --git a/addons/web_livechat/i18n/pt.po b/addons/web_livechat/i18n/pt.po
deleted file mode 100644
index 37efea52bc2..00000000000
--- a/addons/web_livechat/i18n/pt.po
+++ /dev/null
@@ -1,36 +0,0 @@
-# Portuguese translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-07-31 22:18+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Portuguese \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "O contrato já está registado no sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "Copy text \t publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Suporte por mensagens instantâneas (chat)"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Ativar suporte através de mensagens instantâneas para quem tenha contrato de "
-#~ "suporte"
diff --git a/addons/web_livechat/i18n/pt_BR.po b/addons/web_livechat/i18n/pt_BR.po
deleted file mode 100644
index d2805aca128..00000000000
--- a/addons/web_livechat/i18n/pt_BR.po
+++ /dev/null
@@ -1,35 +0,0 @@
-# Brazilian Portuguese translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-01-18 02:51+0000\n"
-"Last-Translator: Rafael Sales - http://www.tompast.com.br \n"
-"Language-Team: Brazilian Portuguese \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Esse contato já está registrado no sistema."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Ativa o suporte com conversa ao vivo para quem tem um contrato de manutenção"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Suporte com Conversa ao Vivo"
diff --git a/addons/web_livechat/i18n/ro.po b/addons/web_livechat/i18n/ro.po
deleted file mode 100644
index 135b02804f7..00000000000
--- a/addons/web_livechat/i18n/ro.po
+++ /dev/null
@@ -1,36 +0,0 @@
-# Romanian translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-01-08 19:46+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Romanian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Acel contract este deja inregistrat in sistem."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "contract. garantie_editor"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Asistentă tehnică pentru Discutiile online"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Activează asistenta tehnică pentru discutiile online pentru cei care au un "
-#~ "contract de mentenantă."
diff --git a/addons/web_livechat/i18n/ru.po b/addons/web_livechat/i18n/ru.po
deleted file mode 100644
index 18980689432..00000000000
--- a/addons/web_livechat/i18n/ru.po
+++ /dev/null
@@ -1,36 +0,0 @@
-# Russian translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-03-09 17:08+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Russian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Договор уже зарегистрирован в системе."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Техподдержка в режиме живого общения"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr ""
-#~ "Включить поддержку живого общения для тех, у кого есть договор на "
-#~ "обслуживание"
diff --git a/addons/web_livechat/i18n/sk.po b/addons/web_livechat/i18n/sk.po
deleted file mode 100644
index f3843d4a5dc..00000000000
--- a/addons/web_livechat/i18n/sk.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Slovak translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-02-21 13:46+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Slovak \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr ""
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Live Chat Podpora"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Povoliť live chat podporu pre toho kto má servisnú zmluvu"
diff --git a/addons/web_livechat/i18n/sl.po b/addons/web_livechat/i18n/sl.po
deleted file mode 100644
index bd2f6d5f2c5..00000000000
--- a/addons/web_livechat/i18n/sl.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Slovenian translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-05-22 17:08+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Slovenian \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Ta pogodba je v sistemu že vnešena"
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Pomoč v živo"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Omogoči pomoč v živo za uporabnike z vzdrževalno pogodbo"
diff --git a/addons/web_livechat/i18n/sr@latin.po b/addons/web_livechat/i18n/sr@latin.po
deleted file mode 100644
index 64709d241ad..00000000000
--- a/addons/web_livechat/i18n/sr@latin.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Serbian Latin translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-10-17 13:43+0000\n"
-"Last-Translator: Milan Milosevic \n"
-"Language-Team: Serbian Latin \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr ""
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Omogući podršku ćaskanja uživo za one koji imaju ugovor o održavanju"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Podrška ćaskanja uživo"
diff --git a/addons/web_livechat/i18n/sv.po b/addons/web_livechat/i18n/sv.po
deleted file mode 100644
index 6a8c39ca3d9..00000000000
--- a/addons/web_livechat/i18n/sv.po
+++ /dev/null
@@ -1,28 +0,0 @@
-# Swedish translation for openobject-addons
-# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2012.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2012-06-04 15:38+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Swedish \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Det kontraktet har redan registrerats i systemet."
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
diff --git a/addons/web_livechat/i18n/tr.po b/addons/web_livechat/i18n/tr.po
deleted file mode 100644
index a884c9eea3b..00000000000
--- a/addons/web_livechat/i18n/tr.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Turkish translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-04-26 14:31+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Turkish
\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "Bu sözleşme sistemde zaten kayıtlı !"
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "Canlı Sohbet Desteği"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "Bakım sözleşmesi olanlar için canlı sohbet desteğini açar"
diff --git a/addons/web_livechat/i18n/zh_CN.po b/addons/web_livechat/i18n/zh_CN.po
deleted file mode 100644
index 0c7ae2c3f17..00000000000
--- a/addons/web_livechat/i18n/zh_CN.po
+++ /dev/null
@@ -1,34 +0,0 @@
-# Chinese (Simplified) translation for openobject-addons
-# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
-# This file is distributed under the same license as the openobject-addons package.
-# FIRST AUTHOR , 2011.
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: openobject-addons\n"
-"Report-Msgid-Bugs-To: FULL NAME \n"
-"POT-Creation-Date: 2012-02-08 01:37+0100\n"
-"PO-Revision-Date: 2011-04-08 07:20+0000\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: Chinese (Simplified) \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2012-07-14 06:26+0000\n"
-"X-Generator: Launchpad (build 15614)\n"
-
-#. module: web_livechat
-#: sql_constraint:publisher_warranty.contract:0
-msgid "That contract is already registered in the system."
-msgstr "该合同已登记到系统中。"
-
-#. module: web_livechat
-#: model:ir.model,name:web_livechat.model_publisher_warranty_contract
-msgid "publisher_warranty.contract"
-msgstr "publisher_warranty.contract"
-
-#~ msgid "Live Chat Support"
-#~ msgstr "支持在线交流"
-
-#~ msgid "Enable live chat support for whom have a maintenance contract"
-#~ msgstr "其中一个维护合同支持在线交流"
diff --git a/addons/web_shortcuts/__openerp__.py b/addons/web_shortcuts/__openerp__.py
index ca3119294ed..b587215a501 100644
--- a/addons/web_shortcuts/__openerp__.py
+++ b/addons/web_shortcuts/__openerp__.py
@@ -23,8 +23,8 @@
'version': '1.0',
'category': 'Tools',
'description': """
-Enable shortcuts feature in the web client
-==========================================
+Enable shortcuts feature in the web client.
+===========================================
Add a Shortcut icon in the systray in order to access the user's shortcuts (if any).
Add a Shortcut icon besides the views title in order to add/remove a shortcut.
diff --git a/addons/web_shortcuts/static/src/js/web_shortcuts.js b/addons/web_shortcuts/static/src/js/web_shortcuts.js
index a64675c5211..a2d0fe43596 100644
--- a/addons/web_shortcuts/static/src/js/web_shortcuts.js
+++ b/addons/web_shortcuts/static/src/js/web_shortcuts.js
@@ -79,9 +79,10 @@ instance.web_shortcuts.Shortcuts = instance.web.Widget.extend({
var self = this,
id = $link.data('id');
self.session.active_id = id;
+ // TODO: Use do_action({menu_id: id, type: 'ir.actions.menu'})
self.rpc('/web/menu/action', {'menu_id': id}, function(ir_menu_data) {
if (ir_menu_data.action.length){
- instance.webclient.user_menu.on_action(ir_menu_data.action[0][2]);
+ instance.webclient.user_menu.on_action({action_id: ir_menu_data.action[0][2].id});
}
});
this.$element.find('.oe_systray_shortcuts').trigger('mouseout');
diff --git a/addons/wiki/wiki_view.xml b/addons/wiki/wiki_view.xml
index 700585122d3..b0f0ab0198e 100644
--- a/addons/wiki/wiki_view.xml
+++ b/addons/wiki/wiki_view.xml
@@ -1,15 +1,18 @@
-
-
+
+
-
+
+
+
@@ -38,7 +41,7 @@
+ attrs="{'required':[('method','=','page')], 'readonly':[('method','!=','page')]}"/>
@@ -61,7 +64,7 @@
name="open_wiki_page"
string="Open Wiki Page"
type="object"
- icon="gtk-ok" />
+ icon="gtk-ok"/>
@@ -131,7 +134,7 @@