diff --git a/addons/account/account.py b/addons/account/account.py index ee0da138dafbb7449771538cee22aea3428e4df5..04bf1a02fe9d508d4e4033d365f67ffd9dda5dc7 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -5,6 +5,7 @@ import math from openerp.osv import expression from openerp.tools.float_utils import float_round as round +from openerp.tools.safe_eval import safe_eval as eval from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT from openerp.exceptions import UserError from openerp import api, fields, models, _ diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 9c1595274670d10ddffbcf4a68ffe4135f9a3c76..b3e80bcdd45dd23a59c97c7eaccf124c00a81f9b 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -2,11 +2,13 @@ from openerp import api, fields, models, _ from openerp.osv import expression +from openerp.tools import float_is_zero from openerp.tools import float_compare, float_round from openerp.tools.misc import formatLang from openerp.exceptions import UserError, ValidationError import time +import math class AccountCashboxLine(models.Model): """ Cash Box Details """ diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index e7ec5649f2f5916ea6c39efd4f5d4597c55038b5..a6e6ad0325771cb065a2ede8724c494894808636 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -95,7 +95,7 @@ class AccountInvoice(models.Model): if line.account_id.internal_type in ('receivable', 'payable'): residual_signed += line.amount_residual * sign if line.currency_id == self.currency_id: - residual += line.currency_id and line.amount_residual_currency or line.amount_residual + residual += line.amount_residual_currency if line.currency_id else line.amount_residual else: from_currency = (line.currency_id and line.currency_id.with_context(date=line.date)) or line.company_id.currency_id.with_context(date=line.date) residual += from_currency.compute(line.amount_residual, self.currency_id) diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 4a6f923979cfbddfc2fe64c38ad95f45fecfb0be..b324d06aded9882f0f453f0cb1e6da368ca426cc 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -158,18 +158,23 @@ class AccountInvoiceReport(models.Model): # self._table = account_invoice_report tools.drop_view_if_exists(cr, self._table) cr.execute("""CREATE or REPLACE VIEW %s as ( + WITH currency_rate (currency_id, rate, date_start, date_end) AS ( + SELECT r.currency_id, r.rate, r.name AS date_start, + (SELECT name FROM res_currency_rate r2 + WHERE r2.name > r.name AND + r2.currency_id = r.currency_id + ORDER BY r2.name ASC + LIMIT 1) AS date_end + FROM res_currency_rate r + ) %s FROM ( %s %s %s ) AS sub - JOIN res_currency_rate cr ON (cr.currency_id = sub.currency_id) - WHERE - cr.id IN (SELECT id - FROM res_currency_rate cr2 - WHERE (cr2.currency_id = sub.currency_id) - AND ((sub.date IS NOT NULL AND cr2.name <= sub.date) - OR (sub.date IS NULL AND cr2.name <= NOW())) - ORDER BY name DESC LIMIT 1) + JOIN currency_rate cr ON + (cr.currency_id = sub.currency_id AND + cr.date_start <= COALESCE(sub.date, NOW()) AND + (cr.date_end IS NULL OR cr.date_end > COALESCE(sub.date, NOW()))) )""" % ( self._table, self._select(), self._sub_select(), self._from(), self._group_by())) diff --git a/addons/account/res_config.py b/addons/account/res_config.py index 9a6443854e5120e974e3de047ee87bcc9e381863..b29e0175f1a8d102e5dbdc203bd1294fea670720 100644 --- a/addons/account/res_config.py +++ b/addons/account/res_config.py @@ -83,9 +83,11 @@ class AccountConfigSettings(models.TransientModel): module_account_accountant = fields.Boolean(string='Full accounting features: journals, legal statements, chart of accounts, etc.', help="""If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of Accounts, ...)""") - module_account_asset = fields.Boolean(string='Assets management', - help='This allows you to manage the assets owned by a company or a person.\n' - 'It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.\n' + module_account_asset = fields.Boolean(string='Assets management & Revenue recognition', + help='Asset management: This allows you to manage the assets owned by a company or a person.' + 'It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.\n\n' + 'Revenue recognition: This allows you to manage the Revenue recognition on selling product.' + 'It keeps track of the installment occurred on those revenue recognition, and creates account move for those installment lines.\n' '-This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments, ' 'but not accounting (Journal Items, Chart of Accounts, ...)') module_account_budget = fields.Boolean(string='Budget management', diff --git a/addons/account/static/src/css/account_reconciliation.scss b/addons/account/static/src/css/account_reconciliation.scss index 1e1c3e5ed23b92d64e18a868cab67e2d223c74aa..c2d4973d2598ce39c2bbc22afacb9a0c0b9cc89e 100644 --- a/addons/account/static/src/css/account_reconciliation.scss +++ b/addons/account/static/src/css/account_reconciliation.scss @@ -560,11 +560,10 @@ $aestetic_animation_speed: 300ms; } &.add_line_container { - &:nth-child(2n+1) { - width: 98%; - float: none; - margin: auto; - } + width: 98%; + float: none; + clear: both; + margin: auto; td { text-align: center; diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index 4d999d6df45a0ef2a6a1508f8d85439250bdae48..123bb480224c952d866c9c098107aea0d7324cd7 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -1853,10 +1853,11 @@ var bankStatementReconciliationLine = abstractReconciliationLine.extend({ changePartnerClickHandler: function() { var self = this; - self.$(".change_partner_container").find("input").attr("placeholder", self.st_line.partner_name); - self.$(".change_partner_container").show(); - self.$(".partner_name").hide(); - self.change_partner_field.$drop_down.trigger("click"); + $.when(self.changePartner(false)).then(function(){ + self.$(".change_partner_container").show(); + self.$(".partner_name").hide(); + self.change_partner_field.$drop_down.trigger("click"); + }) }, diff --git a/addons/account/tests/test_search.py b/addons/account/tests/test_search.py index 7a8ceaef3624458259ea66a4be086ed24ee9f722..f79e368d9c239eeffd72ec99cc4eab14e61a9a85 100644 --- a/addons/account/tests/test_search.py +++ b/addons/account/tests/test_search.py @@ -54,3 +54,18 @@ class TestSearch(AccountTestUsers): asale_ids = self.account_model.name_search(name='XX200', operator='ilike', args=[('id', 'in', self.all_ids)]) self.assertEqual(set([self.asale[0]]), set([a[0] for a in asale_ids]), "name_search 'ilike XX200' should have returned Product Sales account only") + + def test_property_unset_search(self): + cr, uid = self.cr, self.uid + res_partner_model = self.registry('res.partner') + account_payment_term_model = self.registry('account.payment.term') + + a_partner = res_partner_model.create(cr, uid, {'name': 'test partner'}) + a_payment_term = account_payment_term_model.create(cr, uid, {'name': 'test payment term'}) + + partner_ids = res_partner_model.search(cr, uid, [('property_payment_term', '=', False), ('id', '=', a_partner)]) + self.assertTrue(partner_ids, "unset property field 'propety_payment_term' should have been found") + + res_partner_model.write(cr, uid, [a_partner], {'property_payment_term': a_payment_term}) + partner_ids = res_partner_model.search(cr, uid, [('property_payment_term', '=', False), ('id', '=', a_partner)]) + self.assertFalse(partner_ids, "set property field 'propety_payment_term' should not have been found") diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 91499cf7f72850b828dbd72cdd704e5848c7b222..799f03fbbf09bcec13c3120fe189cd1ac08758ce 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -545,7 +545,7 @@ <field name="view_mode">tree,form,calendar,graph</field> <field name="view_id" ref="invoice_tree"/> <field name="domain">[('type','in', ['out_invoice', 'out_refund']), ('state', 'not in', ['draft', 'cancel'])]</field> - <field name="context">{'type':False}</field> + <field name="context">{'default_type':'out_invoice', 'type':'out_invoice', 'journal_type': 'sale'}</field> <field name="search_view_id" ref="view_account_invoice_filter"/> </record> diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index 4aaf8b8ca16c4eb2b3903b92ac5def3da180fdbd..cb6645739a4adeb6e48c5d3720345c27085bd907 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from openerp import models, fields, api, _ +from openerp.tools.safe_eval import safe_eval as eval from openerp.exceptions import UserError diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index 605e4379ceec936db52bb71e199d158e2c232c55..17a05e1da90dbe4aa7e5adc63f6763a478913f11 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -669,34 +669,36 @@ class account_analytic_account(osv.osv): context = context or {} journal_obj = self.pool.get('account.journal') + fpos_obj = self.pool['account.fiscal.position'] + partner = contract.partner_id - if not contract.partner_id: + if not partner: raise UserError(_("You must first select a Customer for Contract %s!") % contract.name ) - fpos = contract.partner_id.property_account_position or False + fpos_id = fpos_obj.get_fiscal_position(cr, uid, partner.company_id.id, partner.id, context=context) journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', contract.company_id.id or False)], limit=1) if not journal_ids: raise UserError(_('Please define a sale journal for the company "%s".') % (contract.company_id.name or '', )) - partner_payment_term = contract.partner_id.property_payment_term and contract.partner_id.property_payment_term.id or False + partner_payment_term = partner.property_payment_term and partner.property_payment_term.id or False currency_id = False if contract.pricelist_id: currency_id = contract.pricelist_id.currency_id.id - elif contract.partner_id.property_product_pricelist: - currency_id = contract.partner_id.property_product_pricelist.currency_id.id + elif partner.property_product_pricelist: + currency_id = partner.property_product_pricelist.currency_id.id elif contract.company_id: currency_id = contract.company_id.currency_id.id invoice = { - 'account_id': contract.partner_id.property_account_receivable.id, + 'account_id': partner.property_account_receivable.id, 'type': 'out_invoice', - 'partner_id': contract.partner_id.id, + 'partner_id': partner.id, 'currency_id': currency_id, 'journal_id': len(journal_ids) and journal_ids[0] or False, 'date_invoice': contract.recurring_next_date, 'origin': contract.code, - 'fiscal_position_id': fpos and fpos.id, + 'fiscal_position_id': fpos_id, 'payment_term_id': partner_payment_term, 'company_id': contract.company_id.id or False, } diff --git a/addons/account_asset/__init__.py b/addons/account_asset/__init__.py index 8c1b6b60c164ab395c4110476a2af8695d939b73..f826640892bf51a387844d963162a4470efd620d 100644 --- a/addons/account_asset/__init__.py +++ b/addons/account_asset/__init__.py @@ -1,23 +1,4 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## +# -*- coding: utf-8 -*- import account_asset import account_asset_invoice diff --git a/addons/account_asset/__openerp__.py b/addons/account_asset/__openerp__.py index 010fc167bfce139006ebcb51c0b01c0daaa0461f..7a1f36b6f8cc29eaae2e386bfdea0e0ac83db5a8 100644 --- a/addons/account_asset/__openerp__.py +++ b/addons/account_asset/__openerp__.py @@ -1,36 +1,20 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## +# -*- coding: utf-8 -*- { - 'name': 'Assets Management', + 'name': 'Assets & Revenue Recognitions Management', 'version': '1.0', - 'depends': ['account'], - 'author': 'OpenERP S.A.', + 'depends': ['account_accountant'], + 'author': 'Odoo S.A.', 'description': """ -Financial and accounting asset management. -========================================== +Assets management +================= +Manage assets owned by a company or a person. +Keeps track of depreciations, and creates corresponding journal entries. -This Module manages the assets owned by a company or an individual. It will keep -track of depreciation's occurred on those assets. And it allows to create Move's -of the depreciation lines. +Revenue recognition +=================== +Manage revenue recognition on product sales. +Keeps track of the revenue recognition installments, and creates corresponding journal entries. """, 'website': 'https://www.odoo.com/page/accounting', @@ -39,23 +23,23 @@ of the depreciation lines. 'demo': [ 'account_asset_demo.yml', ], - 'test': [ - '../account/test/account_minimal_test.xml', - 'test/account_asset_demo_test.xml', - 'test/account_asset_demo.yml', - 'test/account_asset.yml', - 'test/account_asset_wizard.yml', - ], + # 'test': [ + # '../account/test/account_minimal_test.xml', + # 'test/account_asset_demo_test.xml', + # ], 'data': [ 'security/account_asset_security.xml', 'security/ir.model.access.csv', 'wizard/account_asset_change_duration_view.xml', 'wizard/wizard_asset_compute_view.xml', - 'account_asset_view.xml', - 'account_asset_invoice_view.xml', + 'views/account_asset_view.xml', + 'views/account_asset_invoice_view.xml', 'report/account_asset_report_view.xml', + 'views/account_asset.xml', + ], + 'qweb': [ + "static/src/xml/account_asset_template.xml", ], - 'auto_install': False, 'installable': True, 'application': False, } diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index 7a326a8df7a2545f583c9c87e06c33791eda7b88..ebe22bc408c632d20b1ecdea28f903030aea0609 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -1,335 +1,313 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## - -import time -from datetime import datetime +# -*- coding: utf-8 -*- + +from datetime import date, datetime from dateutil.relativedelta import relativedelta -from openerp.osv import fields, osv import openerp.addons.decimal_precision as dp -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from openerp import api, fields, models, _ +from openerp.exceptions import UserError, ValidationError +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF + -class account_asset_category(osv.osv): +class AccountAssetCategory(models.Model): _name = 'account.asset.category' _description = 'Asset category' - _columns = { - 'name': fields.char('Name', required=True, select=1), - 'note': fields.text('Note'), - 'account_analytic_id': fields.many2one('account.analytic.account', 'Analytic account'), - 'account_asset_id': fields.many2one('account.account', 'Asset Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)]), - 'account_depreciation_id': fields.many2one('account.account', 'Depreciation Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)]), - 'account_expense_depreciation_id': fields.many2one('account.account', 'Depr. Expense Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)]), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'method': fields.selection([('linear','Linear: Computed on basis of Gross Value / Number of Depreciations'),('degressive','Degressive: Computed on basis of Residual Value * Degressive Factor')], 'Computation Method', required=True, help="Choose the method to use to compute the amount of depreciation lines.\n"\ - " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \ - " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"), - 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"), - 'method_period': fields.integer('Period Length', help="State here the time between 2 depreciations, in months", required=True), - 'method_progress_factor': fields.float('Degressive Factor'), - 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, - help="Choose the method to use to compute the dates and number of depreciation lines.\n"\ - " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \ - " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."), - 'method_end': fields.date('Ending date'), - 'prorata':fields.boolean('Prorata Temporis', help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January'), - 'open_asset': fields.boolean('Skip Draft State', help="Check this if you want to automatically confirm the assets of this category when created by invoices."), - } - - _defaults = { - 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.category', context=context), - 'method': 'linear', - 'method_number': 5, - 'method_time': 'number', - 'method_period': 12, - 'method_progress_factor': 0.3, - } - - def onchange_account_asset(self, cr, uid, ids, account_asset_id, context=None): - res = {'value':{}} - if account_asset_id: - res['value'] = {'account_depreciation_id': account_asset_id} - return res + active = fields.Boolean(default=True) + name = fields.Char(required=True, index=True) + note = fields.Text() + account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') + account_asset_id = fields.Many2one('account.account', string='Asset Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)]) + account_income_recognition_id = fields.Many2one('account.account', string='Recognition Income Account', domain=[('internal_type','=','other'), ('deprecated', '=', False)], oldname='account_expense_depreciation_id') + account_depreciation_id = fields.Many2one('account.account', string='Depreciation Account', required=True, domain=[('internal_type','=','other'), ('deprecated', '=', False)]) + journal_id = fields.Many2one('account.journal', string='Journal', required=True) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']._company_default_get('account.asset.category')) + method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, default='linear', + help="Choose the method to use to compute the amount of depreciation lines.\n" + " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" + " * Degressive: Calculated on basis of: Residual Value * Degressive Factor") + method_number = fields.Integer(string='Number of Depreciations', default=5, help="The number of depreciations needed to depreciate your asset") + method_period = fields.Integer(string='Period Length', default=1, help="State here the time between 2 depreciations, in months", required=True) + method_progress_factor = fields.Float('Degressive Factor', default=0.3) + method_time = fields.Selection([('number', 'Number of Depreciations'), ('end', 'Ending Date')], string='Time Method', required=True, default='number', + help="Choose the method to use to compute the dates and number of depreciation lines.\n" + " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" + " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") + method_end = fields.Date('Ending date') + prorata = fields.Boolean(string='Prorata Temporis', help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first of January') + open_asset = fields.Boolean(string='Skip Draft State', help="Check this if you want to automatically confirm the assets of this category when created by invoices.") + type = fields.Selection([('sale', 'Sale: Revenue Recognition'), ('purchase', 'Purchase: Asset')], required=True, index=True, default='purchase') + + @api.onchange('account_asset_id') + def onchange_account_asset(self): + self.account_depreciation_id = self.account_asset_id + + @api.onchange('type') + def onchange_type(self): + if self.type == 'sale': + self.prorata = True + self.method_period = 1 + else: + self.method_period = 12 -class account_asset_asset(osv.osv): +class AccountAssetAsset(models.Model): _name = 'account.asset.asset' - _description = 'Asset' - - def unlink(self, cr, uid, ids, context=None): - for asset in self.browse(cr, uid, ids, context=context): - if asset.account_move_line_ids: - raise UserError(_('You cannot delete an asset that contains posted depreciation lines.')) - return super(account_asset_asset, self).unlink(cr, uid, ids, context=context) - - def _get_last_depreciation_date(self, cr, uid, ids, context=None): + _description = 'Asset/Revenue Recognition' + _inherit = ['mail.thread', 'ir.needaction_mixin'] + + account_move_line_ids = fields.One2many('account.move.line', 'asset_id', string='Entries', readonly=True, states={'draft': [('readonly', False)]}) + entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries') + name = fields.Char(string='Asset/Deferred Revenue Name', required=True, readonly=True, states={'draft': [('readonly', False)]}) + code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['ir.sequence'].next_by_code('account.asset.code')) + value = fields.Float(string='Gross Value', required=True, readonly=True, digits=0, states={'draft': [('readonly', False)]}, oldname='purchase_value') + currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, + default=lambda self: self.env.user.company_id.currency_id.id) + company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, + default=lambda self: self.env['res.company']._company_default_get('account.asset.asset')) + note = fields.Text() + category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]}) + date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, oldname="purchase_date") + state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, copy=False, default='draft', + help="When an asset is created, the status is 'Draft'.\n" + "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" + "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status.") + active = fields.Boolean(default=True) + partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) + method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear', + help="Choose the method to use to compute the amount of depreciation lines.\n * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" + " * Degressive: Calculated on basis of: Residual Value * Degressive Factor") + method_number = fields.Integer(string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset") + method_period = fields.Integer(string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]}, + help="The amount of time between two depreciations, in months") + method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) + method_progress_factor = fields.Float(string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]}) + value_residual = fields.Float(compute='_amount_residual', method=True, digits=0, string='Residual Value') + method_time = fields.Selection([('number', 'Number of Depreciations'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]}, + help="Choose the method to use to compute the dates and number of depreciation lines.\n" + " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" + " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") + prorata = fields.Boolean(string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]}, + help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year') + history_ids = fields.One2many('account.asset.history', 'asset_id', string='History', readonly=True) + depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]}) + salvage_value = fields.Float(string='Salvage Value', digits=0, readonly=True, states={'draft': [('readonly', False)]}, + help="It is the amount you plan to have that you cannot depreciate.") + invoice_id = fields.Many2one('account.invoice', string='Invoice', states={'draft': [('readonly', False)]}, copy=False) + type = fields.Selection(related="category_id.type", string='Type', required=True, default='purchase') + + @api.multi + def unlink(self): + for asset in self: + if asset.state in ['open', 'close']: + raise UserError(_('You cannot delete a document is in %s state.') % (asset.state,)) + if asset.account_move_line_ids: + raise UserError(_('You cannot delete a document that contains posted lines.')) + return super(AccountAssetAsset, self).unlink() + @api.multi + def _get_last_depreciation_date(self): """ @param id: ids of a account.asset.asset objects @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset """ - cr.execute(""" - SELECT a.id as id, COALESCE(MAX(l.date),a.purchase_date) AS date + self.env.cr.execute(""" + SELECT a.id as id, COALESCE(MAX(l.date),a.date) AS date FROM account_asset_asset a LEFT JOIN account_move_line l ON (l.asset_id = a.id) WHERE a.id IN %s - GROUP BY a.id, a.purchase_date """, (tuple(ids),)) - return dict(cr.fetchall()) + GROUP BY a.id, a.date """, (tuple(self.ids),)) + result = dict(self.env.cr.fetchall()) + return result - def _compute_board_amount(self, cr, uid, asset, i, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date, context=None): - #by default amount = 0 + def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date): amount = 0 - if i == undone_dotation_number: + if sequence == undone_dotation_number: amount = residual_amount else: - if asset.method == 'linear': + if self.method == 'linear': amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) - if asset.prorata: - amount = amount_to_depr / asset.method_number + if self.prorata and self.category_id.type == 'purchase': + amount = amount_to_depr / self.method_number days = total_days - float(depreciation_date.strftime('%j')) - if i == 1: - amount = (amount_to_depr / asset.method_number) / total_days * days - elif i == undone_dotation_number: - amount = (amount_to_depr / asset.method_number) / total_days * (total_days - days) - elif asset.method == 'degressive': - amount = residual_amount * asset.method_progress_factor - if asset.prorata: + if sequence == 1: + amount = (amount_to_depr / self.method_number) / total_days * days + elif sequence == undone_dotation_number: + amount = (amount_to_depr / self.method_number) / total_days * (total_days - days) + elif self.method == 'degressive': + amount = residual_amount * self.method_progress_factor + if self.prorata: days = total_days - float(depreciation_date.strftime('%j')) - if i == 1: - amount = (residual_amount * asset.method_progress_factor) / total_days * days - elif i == undone_dotation_number: - amount = (residual_amount * asset.method_progress_factor) / total_days * (total_days - days) + if sequence == 1: + amount = (residual_amount * self.method_progress_factor) / total_days * days + elif sequence == undone_dotation_number: + amount = (residual_amount * self.method_progress_factor) / total_days * (total_days - days) return amount - def _compute_board_undone_dotation_nb(self, cr, uid, asset, depreciation_date, total_days, context=None): - undone_dotation_number = asset.method_number - if asset.method_time == 'end': - end_date = datetime.strptime(asset.method_end, '%Y-%m-%d') + def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): + undone_dotation_number = self.method_number + if self.method_time == 'end': + end_date = datetime.strptime(self.method_end, DF).date() undone_dotation_number = 0 while depreciation_date <= end_date: - depreciation_date = (datetime(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+asset.method_period)) + depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) undone_dotation_number += 1 - if asset.prorata: + if self.prorata and self.category_id.type == 'purchase': undone_dotation_number += 1 return undone_dotation_number - def compute_depreciation_board(self, cr, uid, ids, context=None): - depreciation_lin_obj = self.pool.get('account.asset.depreciation.line') - currency_obj = self.pool.get('res.currency') - for asset in self.browse(cr, uid, ids, context=context): - if asset.value_residual == 0.0: - continue - posted_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_check', '=', True)],order='depreciation_date desc') - old_depreciation_line_ids = depreciation_lin_obj.search(cr, uid, [('asset_id', '=', asset.id), ('move_id', '=', False)]) - if old_depreciation_line_ids: - depreciation_lin_obj.unlink(cr, uid, old_depreciation_line_ids, context=context) - - amount_to_depr = residual_amount = asset.value_residual - if asset.prorata: - depreciation_date = datetime.strptime(self._get_last_depreciation_date(cr, uid, [asset.id], context)[asset.id], '%Y-%m-%d') + @api.multi + def compute_depreciation_board(self): + self.ensure_one() + + posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check) + unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check) + + # Remove old unposted depreciation lines. We cannot use unlink() with One2many field + commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] + + if self.value != 0.0: + amount_to_depr = residual_amount = self.value_residual + if self.prorata: + depreciation_date = datetime.strptime(self._get_last_depreciation_date()[self.id], DF).date() else: - # depreciation_date = 1st January of purchase year - purchase_date = datetime.strptime(asset.purchase_date, '%Y-%m-%d') - #if we already have some previous validated entries, starting date isn't 1st January but last entry + method period - if (len(posted_depreciation_line_ids)>0): - last_depreciation_date = datetime.strptime(depreciation_lin_obj.browse(cr,uid,posted_depreciation_line_ids[0],context=context).depreciation_date, '%Y-%m-%d') - depreciation_date = (last_depreciation_date+relativedelta(months=+asset.method_period)) + # depreciation_date = 1st of January of purchase year + asset_date = datetime.strptime(self.date, DF).date() + # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period + if posted_depreciation_line_ids and posted_depreciation_line_ids[0].depreciation_date: + last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[0].depreciation_date, DF).date() + depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: - depreciation_date = datetime(purchase_date.year, 1, 1) + depreciation_date = asset_date day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year total_days = (year % 4) and 365 or 366 - undone_dotation_number = self._compute_board_undone_dotation_nb(cr, uid, asset, depreciation_date, total_days, context=context) + undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) for x in range(len(posted_depreciation_line_ids), undone_dotation_number): - i = x + 1 - amount = self._compute_board_amount(cr, uid, asset, i, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date, context=context) + sequence = x + 1 + amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) + amount = self.currency_id.round(amount) residual_amount -= amount vals = { - 'amount': amount, - 'asset_id': asset.id, - 'sequence': i, - 'name': str(asset.id) +'/' + str(i), - 'remaining_value': residual_amount, - 'depreciated_value': (asset.purchase_value - asset.salvage_value) - (residual_amount + amount), - 'depreciation_date': depreciation_date.strftime('%Y-%m-%d'), + 'amount': amount, + 'asset_id': self.id, + 'sequence': sequence, + 'name': (self.code or str(self.id)) + '/' + str(sequence), + 'remaining_value': residual_amount, + 'depreciated_value': (self.value - self.salvage_value) - (residual_amount + amount), + 'depreciation_date': depreciation_date.strftime(DF), } - depreciation_lin_obj.create(cr, uid, vals, context=context) + commands.append((0, False, vals)) # Considering Depr. Period as months - depreciation_date = (datetime(year, month, day) + relativedelta(months=+asset.method_period)) + depreciation_date = date(year, month, day) + relativedelta(months=+self.method_period) day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year - return True - def validate(self, cr, uid, ids, context=None): - if context is None: - context = {} - return self.write(cr, uid, ids, { - 'state':'open' - }, context) - - def set_to_close(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'close'}, context=context) - - def set_to_draft(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'draft'}, context=context) - - def _amount_residual(self, cr, uid, ids, name, args, context=None): - cr.execute("""SELECT - l.asset_id as id, SUM(abs(l.debit-l.credit)) AS amount - FROM - account_move_line l - WHERE - l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),)) - res=dict(cr.fetchall()) - for asset in self.browse(cr, uid, ids, context): - company_currency = asset.company_id.currency_id.id - current_currency = asset.currency_id.id - amount = self.pool['res.currency'].compute(cr, uid, company_currency, current_currency, res.get(asset.id, 0.0), context=context) - res[asset.id] = asset.purchase_value - amount - asset.salvage_value - for id in ids: - res.setdefault(id, 0.0) - return res + self.write({'depreciation_line_ids': commands}) - def onchange_company_id(self, cr, uid, ids, company_id=False, context=None): - val = {} - if company_id: - company = self.pool.get('res.company').browse(cr, uid, company_id, context=context) - val['currency_id'] = company.currency_id.id - return {'value': val} - - def onchange_purchase_salvage_value(self, cr, uid, ids, purchase_value, salvage_value, context=None): - val = {} - for asset in self.browse(cr, uid, ids, context=context): - if purchase_value: - val['value_residual'] = purchase_value - salvage_value - if salvage_value: - val['value_residual'] = purchase_value - salvage_value - return {'value': val} - def _entry_count(self, cr, uid, ids, field_name, arg, context=None): - MoveLine = self.pool('account.move.line') - return { - asset_id: MoveLine.search_count(cr, uid, [('asset_id', '=', asset_id)], context=context) - for asset_id in ids - } - _columns = { - 'account_move_line_ids': fields.one2many('account.move.line', 'asset_id', 'Entries', readonly=True, states={'draft':[('readonly',False)]}), - 'entry_count': fields.function(_entry_count, string='# Asset Entries', type='integer'), - 'name': fields.char('Asset Name', required=True, readonly=True, states={'draft':[('readonly',False)]}), - 'code': fields.char('Reference', size=32, readonly=True, states={'draft':[('readonly',False)]}), - 'purchase_value': fields.float('Gross Value', required=True, readonly=True, states={'draft':[('readonly',False)]}), - 'currency_id': fields.many2one('res.currency','Currency',required=True, readonly=True, states={'draft':[('readonly',False)]}), - 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, states={'draft':[('readonly',False)]}), - 'note': fields.text('Note'), - 'category_id': fields.many2one('account.asset.category', 'Asset Category', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}), - 'purchase_date': fields.date('Purchase Date', required=True, readonly=True, states={'draft':[('readonly',False)]}), - 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', required=True, copy=False, - help="When an asset is created, the status is 'Draft'.\n" \ - "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" \ - "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status."), - 'active': fields.boolean('Active'), - 'partner_id': fields.many2one('res.partner', 'Supplier', readonly=True, states={'draft':[('readonly',False)]}), - 'method': fields.selection([('linear','Linear'),('degressive','Degressive')], 'Computation Method', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="Choose the method to use to compute the amount of depreciation lines.\n"\ - " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" \ - " * Degressive: Calculated on basis of: Residual Value * Degressive Factor"), - 'method_number': fields.integer('Number of Depreciations', readonly=True, states={'draft':[('readonly',False)]}, help="The number of depreciations needed to depreciate your asset"), - 'method_period': fields.integer('Number of Months in a Period', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="The amount of time between two depreciations, in months"), - 'method_end': fields.date('Ending Date', readonly=True, states={'draft':[('readonly',False)]}), - 'method_progress_factor': fields.float('Degressive Factor', readonly=True, states={'draft':[('readonly',False)]}), - 'value_residual': fields.function(_amount_residual, method=True, digits=0, string='Residual Value'), - 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, readonly=True, states={'draft':[('readonly',False)]}, - help="Choose the method to use to compute the dates and number of depreciation lines.\n"\ - " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \ - " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."), - 'prorata':fields.boolean('Prorata Temporis', readonly=True, states={'draft':[('readonly',False)]}, help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January'), - 'history_ids': fields.one2many('account.asset.history', 'asset_id', 'History', readonly=True), - 'depreciation_line_ids': fields.one2many('account.asset.depreciation.line', 'asset_id', 'Depreciation Lines', readonly=True, states={'draft':[('readonly',False)],'open':[('readonly',False)]}), - 'salvage_value': fields.float('Salvage Value', digits=0, help="It is the amount you plan to have that you cannot depreciate.", readonly=True, states={'draft':[('readonly',False)]}), - } - _defaults = { - 'code': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'account.asset.code'), - 'purchase_date': lambda obj, cr, uid, context: time.strftime('%Y-%m-%d'), - 'active': True, - 'state': 'draft', - 'method': 'linear', - 'method_number': 5, - 'method_time': 'number', - 'method_period': 12, - 'method_progress_factor': 0.3, - 'currency_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.currency_id.id, - 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'account.asset.asset',context=context), - } - - def _check_recursion(self, cr, uid, ids, context=None, parent=None): - return super(account_asset_asset, self)._check_recursion(cr, uid, ids, context=context, parent=parent) - - def _check_prorata(self, cr, uid, ids, context=None): - for asset in self.browse(cr, uid, ids, context=context): - if asset.prorata and asset.method_time != 'number': - return False return True - _constraints = [ - (_check_prorata, 'Prorata temporis can be applied only for time method "number of depreciations".', ['prorata']), - ] - - def onchange_category_id(self, cr, uid, ids, category_id, context=None): - res = {'value':{}} - asset_categ_obj = self.pool.get('account.asset.category') + @api.multi + def validate(self): + self.write({'state': 'open'}) + + @api.multi + def set_to_close(self): + unposted_dep_line = self.env['account.asset.depreciation.line'].search_count( + [('asset_id', 'in', self.ids), ('move_check', '=', False)]) + if unposted_dep_line: + raise UserError(_('You cannot close a document which has unposted lines.')) + self.message_post(body=_("Document closed.")) + self.write({'state': 'close'}) + + @api.multi + def set_to_draft(self): + self.write({'state': 'draft'}) + + @api.one + @api.depends('value', 'salvage_value', 'depreciation_line_ids') + def _amount_residual(self): + total_amount = 0.0 + for line in self.depreciation_line_ids: + if line.move_check: + total_amount += line.amount + self.value_residual = self.value - total_amount - self.salvage_value + + @api.onchange('company_id') + def onchange_company_id(self): + self.currency_id = self.company_id.currency_id.id + + @api.multi + @api.depends('account_move_line_ids') + def _entry_count(self): + for asset in self: + asset.entry_count = self.env['account.move.line'].search_count([('asset_id', '=', asset.id)]) + + @api.one + @api.constrains('prorata', 'method_time') + def _check_prorata(self): + if self.prorata and self.method_time != 'number': + raise ValidationError(_('Prorata temporis can be applied only for time method "number of depreciations".')) + + @api.onchange('category_id') + def onchange_category_id(self): + vals = self.onchange_category_id_values(self.category_id.id) + # We cannot use 'write' on an object that doesn't exist yet + if vals: + for k, v in vals['value'].iteritems(): + setattr(self, k, v) + + def onchange_category_id_values(self, category_id): if category_id: - category_obj = asset_categ_obj.browse(cr, uid, category_id, context=context) - res['value'] = { - 'method': category_obj.method, - 'method_number': category_obj.method_number, - 'method_time': category_obj.method_time, - 'method_period': category_obj.method_period, - 'method_progress_factor': category_obj.method_progress_factor, - 'method_end': category_obj.method_end, - 'prorata': category_obj.prorata, + category = self.env['account.asset.category'].browse(category_id) + return { + 'value': { + 'method': category.method, + 'method_number': category.method_number, + 'method_time': category.method_time, + 'method_period': category.method_period, + 'method_progress_factor': category.method_progress_factor, + 'method_end': category.method_end, + 'prorata': category.prorata, + } } - return res - def onchange_method_time(self, cr, uid, ids, method_time='number', context=None): - res = {'value': {}} - if method_time != 'number': - res['value'] = {'prorata': False} + @api.onchange('method_time') + def onchange_method_time(self): + if self.method_time != 'number': + self.prorata = False + + @api.multi + def copy_data(self, default=None): + if default is None: + default = {} + default['name'] = self.name + _(' (copy)') + return super(AccountAssetAsset, self).copy_data(default)[0] + + @api.multi + def _compute_entries(self, date): + depreciation_ids = self.env['account.asset.depreciation.line'].search([ + ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), + ('move_check', '=', False)]) + return depreciation_ids.create_move() + + @api.model + def create(self, vals): + asset = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals) + asset.compute_depreciation_board() + return asset + + @api.multi + def write(self, vals): + res = super(AccountAssetAsset, self).write(vals) + if 'depreciation_line_ids' not in vals: + self.compute_depreciation_board() return res - def _compute_entries(self, cr, uid, ids, date, context=None): - result = [] - depreciation_obj = self.pool.get('account.asset.depreciation.line') - depreciation_ids = depreciation_obj.search(cr, uid, [('asset_id', 'in', ids), ('depreciation_date', '<=', date), ('move_check', '=', False)], context=context) - context = dict(context or {}, depreciation_date=time.strftime('%Y-%m-%d')) - return depreciation_obj.create_move(cr, uid, depreciation_ids, context=context) - - def create(self, cr, uid, vals, context=None): - asset_id = super(account_asset_asset, self).create(cr, uid, vals, context=context) - self.compute_depreciation_board(cr, uid, [asset_id], context=context) - return asset_id - - def open_entries(self, cr, uid, ids, context=None): - context = dict(context or {}, search_default_asset_id=ids, default_asset_id=ids) + @api.multi + def open_entries(self): return { 'name': _('Journal Items'), 'view_type': 'form', @@ -337,78 +315,74 @@ class account_asset_asset(osv.osv): 'res_model': 'account.move.line', 'view_id': False, 'type': 'ir.actions.act_window', - 'context': context, + 'context': dict(self.env.context or {}, search_default_asset_id=self.id, default_asset_id=self.id), } -class account_asset_depreciation_line(osv.osv): +class AccountAssetDepreciationLine(models.Model): _name = 'account.asset.depreciation.line' _description = 'Asset depreciation line' - def _get_move_check(self, cr, uid, ids, name, args, context=None): - res = {} - for line in self.browse(cr, uid, ids, context=context): - res[line.id] = bool(line.move_id) - return res - - _columns = { - 'name': fields.char('Depreciation Name', required=True, select=1), - 'sequence': fields.integer('Sequence', required=True), - 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True, ondelete='cascade'), - 'parent_state': fields.related('asset_id', 'state', type='char', string='State of Asset'), - 'amount': fields.float('Current Depreciation', digits=0, required=True), - 'remaining_value': fields.float('Next Period Depreciation', digits=0, required=True), - 'depreciated_value': fields.float('Amount Already Depreciated', required=True), - 'depreciation_date': fields.date('Depreciation Date', select=1), - 'move_id': fields.many2one('account.move', 'Depreciation Entry'), - 'move_check': fields.function(_get_move_check, method=True, type='boolean', string='Posted', store=True) - } - - def create_move(self, cr, uid, ids, context=None): - context = dict(context or {}) - can_close = False - asset_obj = self.pool.get('account.asset.asset') - move_obj = self.pool.get('account.move') - move_line_obj = self.pool.get('account.move.line') - currency_obj = self.pool.get('res.currency') + name = fields.Char(string='Depreciation Name', required=True, index=True) + sequence = fields.Integer(required=True) + asset_id = fields.Many2one('account.asset.asset', string='Asset', required=True, ondelete='cascade') + parent_state = fields.Selection(related='asset_id.state', string='State of Asset') + amount = fields.Float(string='Current Depreciation', digits=0, required=True) + remaining_value = fields.Float(string='Next Period Depreciation', digits=0, required=True) + depreciated_value = fields.Float(string='Amount Already Depreciated', required=True) + depreciation_date = fields.Date('Depreciation Date', index=True) + move_id = fields.Many2one('account.move', string='Depreciation Entry') + move_check = fields.Boolean(compute='_get_move_check', string='Posted', track_visibility='always', store=True) + + @api.one + @api.depends('move_id') + def _get_move_check(self): + self.move_check = bool(self.move_id) + + @api.multi + def create_move(self): created_move_ids = [] asset_ids = [] - for line in self.browse(cr, uid, ids, context=context): - depreciation_date = context.get('depreciation_date') or line.depreciation_date or time.strftime('%Y-%m-%d') - company_currency = line.asset_id.company_id.currency_id.id - current_currency = line.asset_id.currency_id.id - context.update({'date': depreciation_date}) - amount = currency_obj.compute(cr, uid, current_currency, company_currency, line.amount, context=context) - sign = (line.asset_id.category_id.journal_id.type == 'purchase' and 1) or -1 + for line in self: + depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) + company_currency = line.asset_id.company_id.currency_id + current_currency = line.asset_id.currency_id + amount = company_currency.compute(line.amount, current_currency) + sign = (line.asset_id.category_id.journal_id.type == 'purchase' or line.asset_id.category_id.journal_id.type == 'sale' and 1) or -1 asset_name = line.asset_id.name reference = line.name journal_id = line.asset_id.category_id.journal_id.id partner_id = line.asset_id.partner_id.id + categ_type = line.asset_id.category_id.type + debit_account = line.asset_id.category_id.account_asset_id.id + credit_account = line.asset_id.category_id.account_depreciation_id.id move_line_1 = { - 'name': asset_name, + 'name': asset_name or reference, 'ref': reference, - 'account_id': line.asset_id.category_id.account_depreciation_id.id, + 'account_id': credit_account, 'debit': 0.0, 'credit': amount, 'journal_id': journal_id, 'partner_id': partner_id, - 'currency_id': company_currency != current_currency and current_currency or False, + 'currency_id': company_currency != current_currency and current_currency or False, 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0, + 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id if categ_type == 'sale' else False, 'date': depreciation_date, + 'asset_id': line.asset_id.id if categ_type == 'sale' else False, } move_line_2 = { - 'name': asset_name, + 'name': asset_name or reference, 'ref': reference, - 'account_id': line.asset_id.category_id.account_expense_depreciation_id.id, + 'account_id': debit_account, 'credit': 0.0, 'debit': amount, 'journal_id': journal_id, 'partner_id': partner_id, - 'currency_id': company_currency != current_currency and current_currency or False, + 'currency_id': company_currency != current_currency and current_currency or False, 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0, - 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id, + 'analytic_account_id': line.asset_id.category_id.account_analytic_id.id if categ_type == 'purchase' else False, 'date': depreciation_date, - 'asset_id': line.asset_id.id + 'asset_id': line.asset_id.id if categ_type == 'purchase' else False } move_vals = { 'name': asset_name, @@ -417,43 +391,67 @@ class account_asset_depreciation_line(osv.osv): 'journal_id': line.asset_id.category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } - move_id = move_obj.create(cr, uid, move_vals, context=context) - self.write(cr, uid, line.id, {'move_id': move_id}, context=context) - created_move_ids.append(move_id) - move_obj.post(cr, uid, [move_id], context=context) - asset_ids.append(line.asset_id.id) + move = self.env['account.move'].create(move_vals) + line.write({'move_id': move.id, 'move_check': True}) + created_move_ids.append(move.id) + move.post() + asset_ids.append(line.asset_id) + partner_name = line.asset_id.partner_id.name + currency_name = line.asset_id.currency_id.name + + def _format_message(message_description, tracked_values): + message = '' + if message_description: + message = '<span>%s</span>' % message_description + for name, values in tracked_values.iteritems(): + message += '<div> • <b>%s</b>: ' % name + message += '%s</div>' % values + return message + + msg_values = {_('Currency'): currency_name, _('Amount'): line.amount} + if partner_name: + msg_values[_('Partner')] = partner_name + msg = _format_message(_('Depreciation line posted.'), msg_values) + line.asset_id.message_post(body=msg) # we re-evaluate the assets to determine whether we can close them - for asset in asset_obj.browse(cr, uid, list(set(asset_ids)), context=context): - if currency_obj.is_zero(cr, uid, asset.currency_id, asset.value_residual): + for asset in asset_ids: + if asset.currency_id.is_zero(asset.value_residual): + asset.message_post(body=_("Document closed.")) asset.write({'state': 'close'}) + asset.compute_depreciation_board() return created_move_ids + @api.multi + def unlink(self): + for record in self: + if record.move_check: + if record.asset_id.category_id.type == 'purchase': + msg = _("You cannot delete posted depreciation lines.") + else: + msg = _("You cannot delete posted installment lines.") + raise UserError(msg) + return super(AccountAssetDepreciationLine, self).unlink() + -class account_move_line(osv.osv): +class AccountMoveLine(models.Model): _inherit = 'account.move.line' - _columns = { - 'asset_id': fields.many2one('account.asset.asset', 'Asset', ondelete="restrict"), - } + asset_id = fields.Many2one('account.asset.asset', string='Asset', ondelete="restrict") + -class account_asset_history(osv.osv): +class AccountAssetHistory(models.Model): _name = 'account.asset.history' _description = 'Asset history' - _columns = { - 'name': fields.char('History name', select=1), - 'user_id': fields.many2one('res.users', 'User', required=True), - 'date': fields.date('Date', required=True), - 'asset_id': fields.many2one('account.asset.asset', 'Asset', required=True), - 'method_time': fields.selection([('number','Number of Depreciations'),('end','Ending Date')], 'Time Method', required=True, - help="The method to use to compute the dates and number of depreciation lines.\n"\ - "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" \ - "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond."), - 'method_number': fields.integer('Number of Depreciations', help="The number of depreciations needed to depreciate your asset"), - 'method_period': fields.integer('Period Length', help="Time in month between two depreciations"), - 'method_end': fields.date('Ending date'), - 'note': fields.text('Note'), - } _order = 'date desc' - _defaults = { - 'date': lambda *args: time.strftime('%Y-%m-%d'), - 'user_id': lambda self, cr, uid, ctx: uid - } + + name = fields.Char(string='History name', index=True) + user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self.env.user) + date = fields.Date(required=True, default=fields.Date.context_today) + asset_id = fields.Many2one('account.asset.asset', string='Asset', required=True) + method_time = fields.Selection([('number', 'Number of Depreciations'), ('end', 'Ending Date')], string='Time Method', required=True, + help="The method to use to compute the dates and number of depreciation lines.\n" + "Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" + "Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") + method_number = fields.Integer(string='Number of Depreciations', help="The number of depreciations needed to depreciate your asset") + method_period = fields.Integer(string='Period Length', help="Time in months between two depreciations") + method_end = fields.Date(string='Ending date') + note = fields.Text() diff --git a/addons/account_asset/account_asset_demo.yml b/addons/account_asset/account_asset_demo.yml index cdfdfbbbd46dd429912de853dbcfe7c9890f0dac..c1bcfd0257c6a92dff86bb278da44aae8dc83b91 100644 --- a/addons/account_asset/account_asset_demo.yml +++ b/addons/account_asset/account_asset_demo.yml @@ -14,7 +14,7 @@ 'method_number': 3, 'account_asset_id': xfa_account_id[0], 'account_depreciation_id': xfa_account_id[0], - 'account_expense_depreciation_id': expense_account_id[0], + 'account_income_recognition_id': expense_account_id[0], } self._update(cr, uid, 'account.asset.category', 'account_asset', vals, 'account_asset_category_fixedassets0') vals = { @@ -23,7 +23,7 @@ 'method_number': 5, 'account_asset_id': xfa_account_id[0], 'account_depreciation_id': xfa_account_id[0], - 'account_expense_depreciation_id': expense_account_id[0], + 'account_income_recognition_id': expense_account_id[0], } self._update(cr, uid, 'account.asset.category', 'account_asset', vals, 'account_asset_category_fixedassets1') vals = { @@ -32,7 +32,7 @@ 'method_period': 12, 'method_number': 5, 'name': "CEO's car", - 'purchase_value': 12000.0, + 'value': 12000.0, 'category_id': ref('account_asset_category_fixedassets0'), } self._update(cr, uid, 'account.asset.asset', 'account_asset', vals, 'account_asset_asset_vehicles0') @@ -41,7 +41,7 @@ 'salvage_value': 0.0, 'method_time': 'end', 'name': 'V6 Engine and 10 inches tires', - 'purchase_value': 2800.0, + 'value': 2800.0, 'category_id': ref('account_asset_category_fixedassets0'), } self._update(cr, uid, 'account.asset.asset', 'account_asset', vals, 'account_asset_asset_cab0') @@ -51,9 +51,9 @@ 'state': 'open', 'method_period': 12, 'method_number': 20, - 'purchase_date': time.strftime('%Y-01-01'), + 'date': time.strftime('%Y-01-01'), 'name': 'Office', - 'purchase_value': 500000.0, + 'value': 500000.0, 'category_id': ref('account_asset_category_fixedassets0'), } self._update(cr, uid, 'account.asset.asset', 'account_asset', vals, 'account_asset_asset_office0') @@ -67,6 +67,6 @@ 'method_number': 6, 'account_asset_id': xfa_account_id[0], 'account_depreciation_id': xfa_account_id[0], - 'account_expense_depreciation_id': sale_account_id[0], + 'account_income_recognition_id': sale_account_id[0], } self._update(cr, uid, 'account.asset.category', 'account_asset', vals, 'account_asset_category_sale') diff --git a/addons/account_asset/account_asset_invoice.py b/addons/account_asset/account_asset_invoice.py index 4f49018cfa1c830bacfaddfdb7ca30db8f4b0b91..b9b5817174572f67d3c0fa877d32b0f8f00327e3 100644 --- a/addons/account_asset/account_asset_invoice.py +++ b/addons/account_asset/account_asset_invoice.py @@ -1,66 +1,97 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## - -from openerp.osv import fields, osv - -class account_invoice(osv.osv): +# -*- coding: utf-8 -*- +from datetime import datetime +from dateutil.relativedelta import relativedelta +from openerp import api, fields, models +import openerp.addons.decimal_precision as dp +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF + + +class AccountInvoice(models.Model): _inherit = 'account.invoice' - def action_number(self, cr, uid, ids, *args): - result = super(account_invoice, self).action_number(cr, uid, ids, *args) - for inv in self.browse(cr, uid, ids): - self.pool.get('account.invoice.line').asset_create(cr, uid, inv.invoice_line_ids) + + @api.multi + def action_move_create(self): + result = super(AccountInvoice, self).action_move_create() + for inv in self: + inv.invoice_line_ids.asset_create() return result - def line_get_convert(self, cr, uid, x, part, context=None): - res = super(account_invoice, self).line_get_convert(cr, uid, x, part, context=context) - res['asset_id'] = x.get('asset_id', False) + @api.model + def line_get_convert(self, line, part): + res = super(AccountInvoice, self).line_get_convert(line, part) + res['asset_id'] = line.get('asset_id', False) return res -class account_invoice_line(osv.osv): - +class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' - _columns = { - 'asset_category_id': fields.many2one('account.asset.category', 'Asset Category'), - } - def asset_create(self, cr, uid, lines, context=None): - context = context or {} - asset_obj = self.pool.get('account.asset.asset') - for line in lines: - if line.asset_category_id: - vals = { - 'name': line.name, - 'code': line.invoice_id.number or False, - 'category_id': line.asset_category_id.id, - 'purchase_value': line.price_subtotal, - 'date': line.invoice_id.date, - 'partner_id': line.invoice_id.partner_id.id, - 'company_id': line.invoice_id.company_id.id, - 'currency_id': line.invoice_id.currency_id.id, - 'purchase_date' : line.invoice_id.date_invoice, - } - changed_vals = asset_obj.onchange_category_id(cr, uid, [], vals['category_id'], context=context) - vals.update(changed_vals['value']) - asset_id = asset_obj.create(cr, uid, vals, context=context) - if line.asset_category_id.open_asset: - asset_obj.validate(cr, uid, [asset_id], context=context) + + asset_category_id = fields.Many2one('account.asset.category', string='Asset Category') + asset_start_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True) + asset_end_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True) + asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True, digits=dp.get_precision('Account'), store=True) + + @api.one + @api.depends('asset_category_id', 'invoice_id.date_invoice') + def _get_asset_date(self): + self.asset_mrr = 0 + self.asset_start_date = False + self.asset_end_date = False + cat = self.asset_category_id + if cat: + months = cat.method_number * cat.method_period + if self.invoice_id.type in ['out_invoice', 'out_refund']: + self.asset_mrr = self.price_subtotal_signed / months + if self.invoice_id.date_invoice: + start_date = datetime.strptime(self.invoice_id.date_invoice, DF).replace(day=1) + end_date = (start_date + relativedelta(months=months, days=-1)) + self.asset_start_date = start_date.strftime(DF) + self.asset_end_date = end_date.strftime(DF) + + @api.one + def asset_create(self): + if self.asset_category_id and self.asset_category_id.method_number > 1: + vals = { + 'name': self.name, + 'code': self.invoice_id.number or False, + 'category_id': self.asset_category_id.id, + 'value': self.price_subtotal, + 'partner_id': self.invoice_id.partner_id.id, + 'company_id': self.invoice_id.company_id.id, + 'currency_id': self.invoice_id.currency_id.id, + 'date': self.asset_start_date or self.invoice_id.date_invoice, + 'invoice_id': self.invoice_id.id, + } + changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id']) + vals.update(changed_vals['value']) + asset = self.env['account.asset.asset'].create(vals) + if self.asset_category_id.open_asset: + asset.validate() return True + + + @api.multi + def product_id_change(self, product, uom_id, qty=0, name='', type='out_invoice', + partner_id=False, fposition_id=False, price_unit=False, currency_id=False, + company_id=None, date_invoice=None): + res = super(AccountInvoiceLine, self).product_id_change( + product, uom_id, qty=qty, name=name, type=type, partner_id=partner_id, + fposition_id=fposition_id, price_unit=price_unit, currency_id=currency_id, + company_id=company_id, date_invoice=date_invoice + ) + if product and res['value']: + product_obj = self.env['product.product'].browse(product) + if type == 'out_invoice': + res['value']['asset_category_id'] = product_obj.product_tmpl_id.deferred_revenue_category_id + elif type == 'in_invoice': + res['value']['asset_category_id'] = product_obj.product_tmpl_id.asset_category_id + + return res + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + asset_category_id = fields.Many2one('account.asset.category', string='Asset Type', ondelete="restrict") + deferred_revenue_category_id = fields.Many2one('account.asset.category', string='Deferred Revenue Type', ondelete="restrict") + diff --git a/addons/account_asset/account_asset_invoice_view.xml b/addons/account_asset/account_asset_invoice_view.xml deleted file mode 100644 index c44d0743e70328e00fe365c3265d6082c7b47666..0000000000000000000000000000000000000000 --- a/addons/account_asset/account_asset_invoice_view.xml +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0"?> -<openerp> - <data> - - <!-- Fiscal Year --> - <record model="ir.ui.view" id="view_account_invoice_asset_form"> - <field name="name">account.invoice.line.form</field> - <field name="model">account.invoice.line</field> - <field name="inherit_id" ref="account.view_invoice_line_form"/> - <field name="arch" type="xml"> - <field name="account_id" position="after"> - <field name="asset_category_id"/> - </field> - </field> - </record> - - <record model="ir.ui.view" id="view_invoice_asset_category"> - <field name="name">account.invoice.supplier.form</field> - <field name="model">account.invoice</field> - <field name="inherit_id" ref="account.invoice_supplier_form"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']" position="before"> - <field name="asset_category_id"/> - </xpath> - </field> - </record> - - </data> -</openerp> diff --git a/addons/account_asset/report/__init__.py b/addons/account_asset/report/__init__.py index 5d59693a7fdef306961d1aa66884da5cf00ef0fb..c60e4c9e043375a7364869cd0315f9e929c1c49a 100644 --- a/addons/account_asset/report/__init__.py +++ b/addons/account_asset/report/__init__.py @@ -1,22 +1,3 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## import account_asset_report diff --git a/addons/account_asset/report/account_asset_report.py b/addons/account_asset/report/account_asset_report.py index 20bce5b17d37fe3627c73e5da454d7a969d42091..938da73efd50632be036e5a4bab3db4fbb4b0dc2 100644 --- a/addons/account_asset/report/account_asset_report.py +++ b/addons/account_asset/report/account_asset_report.py @@ -1,64 +1,46 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## +# -*- coding: utf-8 -*- +from openerp import fields, models, tools -from openerp import tools -from openerp.osv import fields, osv -class asset_asset_report(osv.osv): +class AssetAssetReport(models.Model): _name = "asset.asset.report" _description = "Assets Analysis" _auto = False - _columns = { - 'name': fields.char('Year', required=False, readonly=True), - 'purchase_date': fields.date('Purchase Date', readonly=True), - 'depreciation_date': fields.date('Depreciation Date', readonly=True), - 'asset_id': fields.many2one('account.asset.asset', string='Asset', readonly=True), - 'asset_category_id': fields.many2one('account.asset.category',string='Asset category'), - 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True), - 'state': fields.selection([('draft','Draft'),('open','Running'),('close','Close')], 'Status', readonly=True), - 'depreciation_value': fields.float('Amount of Depreciation Lines', readonly=True), - 'move_check': fields.boolean('Posted', readonly=True), - 'nbr': fields.integer('# of Depreciation Lines', readonly=True), - 'gross_value': fields.float('Gross Amount', readonly=True), - 'posted_value': fields.float('Posted Amount', readonly=True), - 'unposted_value': fields.float('Unposted Amount', readonly=True), - 'company_id': fields.many2one('res.company', 'Company', readonly=True), - } - + + name = fields.Char(string='Year', required=False, readonly=True) + date = fields.Date(readonly=True) + depreciation_date = fields.Date(string='Depreciation Date', readonly=True) + asset_id = fields.Many2one('account.asset.asset', string='Asset', readonly=True) + asset_category_id = fields.Many2one('account.asset.category', string='Asset category', readonly=True) + partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) + state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], string='Status', readonly=True) + depreciation_value = fields.Float(string='Amount of Depreciation Lines', readonly=True) + installment_value = fields.Float(string='Amount of Installment Lines', readonly=True) + move_check = fields.Boolean(string='Posted', readonly=True) + installment_nbr = fields.Integer(string='# of Installment Lines', readonly=True) + depreciation_nbr = fields.Integer(string='# of Depreciation Lines', readonly=True) + gross_value = fields.Float(string='Gross Amount', readonly=True) + posted_value = fields.Float(string='Posted Amount', readonly=True) + unposted_value = fields.Float(string='Unposted Amount', readonly=True) + company_id = fields.Many2one('res.company', string='Company', readonly=True) + def init(self, cr): - tools.drop_view_if_exists(cr, 'asset_asset_report') - cr.execute(""" - create or replace view asset_asset_report as ( - select + tools.drop_view_if_exists(cr, 'asset_asset_report') + cr.execute(""" + create or replace view asset_asset_report as ( + select min(dl.id) as id, dl.name as name, dl.depreciation_date as depreciation_date, - a.purchase_date as purchase_date, + a.date as date, (CASE WHEN (select min(d.id) from account_asset_depreciation_line as d left join account_asset_asset as ac ON (ac.id=d.asset_id) where a.id=ac.id) = min(dl.id) - THEN a.purchase_value + THEN a.value ELSE 0 END) as gross_value, - dl.amount as depreciation_value, + dl.amount as depreciation_value, + dl.amount as installment_value, (CASE WHEN dl.move_check THEN dl.amount ELSE 0 @@ -72,13 +54,13 @@ class asset_asset_report(osv.osv): a.category_id as asset_category_id, a.partner_id as partner_id, a.state as state, - count(dl.*) as nbr, + count(dl.*) as installment_nbr, + count(dl.*) as depreciation_nbr, a.company_id as company_id from account_asset_depreciation_line dl left join account_asset_asset a on (dl.asset_id=a.id) - group by + group by dl.amount,dl.asset_id,dl.depreciation_date,dl.name, - a.purchase_date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id, - a.purchase_value, a.id, a.salvage_value + a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id, + a.value, a.id, a.salvage_value )""") - diff --git a/addons/account_asset/report/account_asset_report_view.xml b/addons/account_asset/report/account_asset_report_view.xml index 5b4b6d2bf8d7aafd34579763e3f8f0a7fdbc8ed9..19f9537562012c9b7a80cc5ffe775416cb163bb9 100644 --- a/addons/account_asset/report/account_asset_report_view.xml +++ b/addons/account_asset/report/account_asset_report_view.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> -<data> +<odoo> <record model="ir.ui.view" id="action_account_asset_report_pivot"> <field name="name">asset.asset.report.pivot</field> @@ -8,7 +7,7 @@ <field name="arch" type="xml"> <pivot string="Assets Analysis" disable_linking="True"> <field name="asset_category_id" type="row"/> - <field name="purchase_date" type="col"/> + <field name="date" type="col"/> <field name="posted_value" type="measure"/> <field name="gross_value" type="measure"/> <field name="depreciation_value" type="measure"/> @@ -21,18 +20,46 @@ <field name="arch" type="xml"> <graph string="Assets Analysis" type="pivot"> <field name="asset_category_id" type="row"/> - <field name="purchase_date" type="col"/> + <field name="date" type="col"/> <field name="gross_value" type="measure"/> </graph> </field> </record> - + + <!-- + Asset Revenue Recognition Graph + --> + <record model="ir.ui.view" id="action_account_revenue_report_pivot"> + <field name="name">asset.asset.report.pivot</field> + <field name="model">asset.asset.report</field> + <field name="arch" type="xml"> + <pivot string="Deferred Revenue Analysis" disable_linking="True"> + <field name="asset_category_id" type="row"/> + <field name="date" type="col"/> + <field name="gross_value" type="measure"/> + </pivot> + </field> + </record> + <record model="ir.ui.view" id="action_account_revenue_report_graph"> + <field name="name">asset.revenue.report.graph</field> + <field name="model">asset.asset.report</field> + <field name="arch" type="xml"> + <graph string="Deferred Revenue Analysis" type="pivot"> + <field name="asset_id" type="row"/> + <field name="installment_nbr" type="measure"/> + <field name="gross_value" type="measure"/> + <field name="installment_value" type="measure"/> + <field name="posted_value" type="measure"/> + </graph> + </field> + </record> + <record id="view_asset_asset_report_search" model="ir.ui.view"> <field name="name">asset.asset.report.search</field> <field name="model">asset.asset.report</field> <field name="arch" type="xml"> <search string="Assets Analysis"> - <field name="purchase_date"/> + <field name="date"/> <field name="depreciation_date"/> <filter string="Draft" domain="[('state','=','draft')]" help="Assets in draft state"/> <filter string="Running" domain="[('state','=','open')]" help="Assets in running state"/> @@ -50,13 +77,46 @@ <filter string="Company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/> <separator/> <filter string="Purchase Month" help="Date of asset purchase" - context="{'group_by':'purchase_date:month'}"/> + context="{'group_by':'date:month'}"/> <filter string="Depreciation Month" help="Date of depreciation" context="{'group_by':'depreciation_date:month'}"/> </group> </search> </field> </record> + + <!-- + Asset Revenue Recognition Search + --> + + <record id="view_asset_revenue_report_search" model="ir.ui.view"> + <field name="name">asset.revenue.report.search</field> + <field name="model">asset.asset.report</field> + <field name="arch" type="xml"> + <search string="Deferred Revenue Analysis"> + <field name="date"/> + <field name="depreciation_date"/> + <filter string="Draft" domain="[('state','=','draft')]" help="Recognition in draft state"/> + <filter string="Running" domain="[('state','=','open')]" help="Assets in running state"/> + <separator/> + <filter string="Posted" name="posted" domain="[('move_check','=',True)]" help="Posted depreciation lines" context="{'unposted_value_visible': 0}"/> + <field name="asset_id"/> + <field name="asset_category_id"/> + <group expand="0" string="Extended Filters..."> + <field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/> + <field name="company_id" groups="base.group_multi_company"/> + </group> + <group expand="1" string="Group By..."> + <filter string="Revenue Recognition" name="revenue" context="{'group_by':'asset_id'}"/> + <filter string="Category" name="category" context="{'group_by':'asset_category_id'}"/> + <filter string="Company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/> + <filter string="Sales Month" domain="[('date','=',time.strftime('%%Y-%%m-%%d'))]" context="{'group_by':'date'}" help="Date of Revenue Sales"/> + <filter string="Revenue Month" name='rev_month' + context="{'group_by':'depreciation_date'}" help="Revenue Month"/> + </group> + </search> + </field> + </record> <record model="ir.actions.act_window" id="action_asset_asset_report"> <field name="name">Assets Analysis</field> @@ -64,18 +124,45 @@ <field name="view_type">form</field> <field name="view_mode">pivot,graph</field> <field name="search_view_id" ref="view_asset_asset_report_search"/> + <field name="domain">[('asset_category_id.type', '=', 'purchase')]</field> <field name="context">{}</field> <!-- force empty --> <field name="help" type="html"> <p> - From this report, you can have an overview on all depreciation. The - tool search can also be used to personalise your Assets reports and - so, match this analysis to your needs; + From this report, you can have an overview on all depreciations. The + search bar can also be used to personalize your assets depreciation reporting. </p> </field> </record> - + + <!-- + Asset Revenue Recognition Action + --> + + <record model="ir.actions.act_window" id="action_asset_revenue_report"> + <field name="name">Deferred Revenue Analysis</field> + <field name="res_model">asset.asset.report</field> + <field name="view_type">form</field> + <field name="view_mode">pivot,graph</field> + <field name="search_view_id" ref="view_asset_revenue_report_search"/> + <field name="domain">[('asset_category_id.type', '=', 'sale')]</field> + <field name="context">{}</field> + <field name="help" type="html"> + <p> + From this report, you can have an overview of your deferred revenue. The + search bar can also be used to personalize your revenue recognition reporting. + </p> + </field> + </record> + <menuitem action="action_asset_asset_report" id="menu_action_asset_asset_report" - parent="account.menu_finance_reports"/> -</data> -</openerp> + parent="account.menu_finance_reports" sequence="21"/> + + <!-- + Asset Revenue Recognition Menu + --> + + <menuitem action="action_asset_revenue_report" + id="menu_action_asset_revenue_report" + parent="account.menu_finance_reports" sequence="20"/> +</odoo> diff --git a/addons/account_asset/security/account_asset_security.xml b/addons/account_asset/security/account_asset_security.xml index 77f218d13b9daee4cbe6bc06b4cdbd026863a963..02320b922dd366d8a47c7095f082c58bbbdb9df4 100644 --- a/addons/account_asset/security/account_asset_security.xml +++ b/addons/account_asset/security/account_asset_security.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> +<odoo> <data noupdate="1"> <record id="account_asset_category_multi_company_rule" model="ir.rule"> <field name="name">Account Asset Category multi-company</field> @@ -16,4 +16,4 @@ </record> </data> -</openerp> +</odoo> diff --git a/addons/account_asset/static/src/js/account_asset.js b/addons/account_asset/static/src/js/account_asset.js new file mode 100644 index 0000000000000000000000000000000000000000..accd57ed3f5e34f6c1251fcfd4d81e152127821d --- /dev/null +++ b/addons/account_asset/static/src/js/account_asset.js @@ -0,0 +1,39 @@ +/* +Purpose : show toggle button on depreciation/installment lines for posted/unposted line. +Details : called in list view with "<button name="create_move" type="object" widget="widgetonbutton"/>", + this will call the method create_move on the object account.asset.depreciation.line +*/ + +odoo.define('account_asset.widget', function(require) { +"use strict"; + +// openerp.account_asset = function (instance) { + // var _t = instance.web._t, + // _lt = instance.web._lt; + // var QWeb = instance.web.qweb; + // instance.web.account_asset = instance.web.account_asset || {}; + +var core = require('web.core'); +var QWeb = core.qweb; +var list_widget_registry = core.list_widget_registry; +var Column = list_widget_registry.get('field'); + +var WidgetOnButton = Column.extend({ + format: function (row_data, options) { + this._super(row_data, options); + this.has_value = !!row_data.move_check.value; + this.parent_state = row_data.parent_state.value; + this.icon = this.has_value ? 'gtk-yes' : 'gtk-no'; // or STOCK_YES and STOCK_NO + this.string = this.has_value ? 'Posted' : 'Unposted'; + var template = this.icon && 'ListView.row.buttonwidget'; + return QWeb.render(template, { + widget: this, + prefix: instance.session.prefix, + disabled: this.has_value, + invisible : true ? this.parent_state !== 'open' : false + }); + }, +}); + +list_widget_registry.add("button.widgetonbutton", WidgetOnButton); +}); diff --git a/addons/account_asset/static/src/xml/account_asset_template.xml b/addons/account_asset/static/src/xml/account_asset_template.xml new file mode 100644 index 0000000000000000000000000000000000000000..ed33ee35f871588b8fb46bac9f5418ee99327b69 --- /dev/null +++ b/addons/account_asset/static/src/xml/account_asset_template.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template"> + <button t-name="ListView.row.buttonwidget" type="button" + t-att-title="widget.string" t-att-disabled="disabled || undefined" + t-att-class="disabled ? 'oe_list_button_disabled' : '',invisible ? 'oe_form_invisible' : ''" + ><img t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png" + t-att-alt="widget.string"/></button> +</templates> \ No newline at end of file diff --git a/addons/account_asset/test/account_asset.yml b/addons/account_asset/test/account_asset.yml deleted file mode 100644 index 708f16786a8fa7f6e83823678269e90bd729cb49..0000000000000000000000000000000000000000 --- a/addons/account_asset/test/account_asset.yml +++ /dev/null @@ -1,35 +0,0 @@ -- - In order to test the process of Account Asset, I perform a action to confirm Account Asset. -- - !python {model: account.asset.asset}: | - self.validate(cr, uid, [ref("account_asset_asset_vehicles_test0")]) -- - I check Asset is now in Open state. -- - !assert {model: account.asset.asset, id: account_asset_asset_vehicles_test0, severity: error, string: Asset should be in Open state}: - - state == 'open' -- - I compute depreciation lines for asset of CEO's Car . -- - !python {model: account.asset.asset}: | - self.compute_depreciation_board(cr, uid, [ref("account_asset_asset_vehicles_test0")]) - # pressing computation button can be remove if creation of depreciation lines while asset is created - value = self.browse(cr, uid, [ref("account_asset_asset_vehicles_test0")])[0] - assert value.method_number == len(value.depreciation_line_ids), 'Depreciation lines not created correctly' -- - I create account move for all depreciation lines. -- - !python {model: account.asset.depreciation.line}: | - ids = self.search(cr, uid, [('asset_id','=',ref('account_asset_asset_vehicles_test0'))]) - self.create_move(cr, uid, ids) -- - I check the move line is created. -- - !python {model: account.asset.asset}: | - asset = self.browse(cr, uid, [ref("account_asset_asset_vehicles_test0")])[0] - assert len(asset.depreciation_line_ids) == len(asset.account_move_line_ids), 'Move lines not created correctly' -- - I Check that After creating all the moves of depreciation lines the state "Close". -- - !assert {model: account.asset.asset, id: account_asset_asset_vehicles_test0}: - - state == 'close' diff --git a/addons/account_asset/test/account_asset_demo.yml b/addons/account_asset/test/account_asset_demo.yml deleted file mode 100644 index 8e8813574a53ac74fa544bc52d2540044e83491d..0000000000000000000000000000000000000000 --- a/addons/account_asset/test/account_asset_demo.yml +++ /dev/null @@ -1,9 +0,0 @@ -- - !record {model: account.asset.category, id: account_asset_category_fixedassets_test0}: - account_asset_id: xfa -- - !record {model: account.asset.asset, id: account_asset_asset_vehicles_test0}: - category_id: account_asset_category_sale_test0 -- - !record {model: account.asset.asset, id: account_asset_asset_vehicles_test0}: - method_number: 5 diff --git a/addons/account_asset/test/account_asset_demo_test.xml b/addons/account_asset/test/account_asset_demo_test.xml index 4fbfd8e4a441fbbd4435ee111a8c3e196d19804a..47af3b0d0cf4bbd4e3313928e7ac7e1b6cb84d59 100644 --- a/addons/account_asset/test/account_asset_demo_test.xml +++ b/addons/account_asset/test/account_asset_demo_test.xml @@ -1,36 +1,49 @@ <?xml version="1.0" ?> -<openerp> +<odoo> <data noupdate="1"> <!-- Asset Category Demo --> <record id="account_asset_category_fixedassets_test0" model="account.asset.category"> - <field name="account_expense_depreciation_id" ref="a_expense"/> + <field name="account_depreciation_id" ref="a_expense"/> <field name="account_asset_id" ref="xfa"/> - <field name="account_depreciation_id" ref="xfa"/> <field name="journal_id" ref="expenses_journal"/> <field name="name">Hardware - 3 Years</field> <field name="method_number">3</field> + <field name="method_period">12</field> </record> <record id="account_asset_category_fixedassets_test1" model="account.asset.category"> - <field name="account_expense_depreciation_id" ref="a_expense"/> + <field name="account_depreciation_id" ref="a_expense"/> <field name="account_asset_id" ref="xfa"/> - <field name="account_depreciation_id" ref="xfa"/> <field name="journal_id" ref="expenses_journal"/> <field name="name">Cars - 5 Years</field> <field name="method_number">5</field> + <field name="method_period">12</field> </record> <record id="account_asset_category_sale_test0" model="account.asset.category"> - <field name="account_expense_depreciation_id" ref="a_sale"/> + <field name="account_depreciation_id" ref="a_sale"/> <field name="account_asset_id" ref="xfa"/> - <field name="account_depreciation_id" ref="xfa"/> - <field name="journal_id" ref="expenses_journal"/> - <field name="name">Revenue Recognition Maintenance Contract - 3 Years</field> + <field name="journal_id" ref="sales_journal"/> + <field name="name">Maintenance Contract - 3 Years</field> <field name="method_number">3</field> + <field name="method_period">12</field> + <field name="prorata" eval="True"/> + <field name="type">sale</field> + </record> + + <record id="account_asset_category_sale1" model="account.asset.category"> + <field name="account_asset_id" ref="xfa"/> + <field name="account_depreciation_id" ref="a_sale"/> + <field name="journal_id" ref="sales_journal"/> + <field name="name">Maintenance Contract - 1 Year</field> + <field name="method_number">12</field> + <field name="method_period">1</field> + <field name="prorata" eval="True"/> + <field name="type">sale</field> </record> <!-- @@ -43,7 +56,7 @@ <field eval="12" name="method_period"/> <field eval="5" name="method_number"/> <field name="name">CEO's Car</field> - <field eval="12000.0" name="purchase_value"/> + <field eval="12000.0" name="value"/> <field name="category_id" ref="account_asset_category_fixedassets_test0"/> </record> @@ -52,7 +65,7 @@ <field eval="0.0" name="salvage_value"/> <field name="method_time">end</field> <field name="name">V6 Engine and 10 inches tires</field> - <field eval="2800.0" name="purchase_value"/> + <field eval="2800.0" name="value"/> <field name="category_id" ref="account_asset_category_fixedassets_test0"/> </record> @@ -61,12 +74,75 @@ <field eval="100000.0" name="salvage_value"/> <field name="state">open</field> <field eval="12" name="method_period"/> - <field eval="20" name="method_number"/> - <field name="purchase_date" eval="time.strftime('%Y-01-01')"/> + <field eval="3" name="method_number"/> + <field name="date" eval="time.strftime('%Y-01-01')"/> <field name="name">Office</field> - <field eval="500000.0" name="purchase_value"/> + <field eval="500000.0" name="value"/> <field name="category_id" ref="account_asset_category_fixedassets_test0"/> </record> - + + <record id="account_asset_asset_pc" model="account.asset.asset"> + <field eval="1" name="prorata"/> + <field name="state">draft</field> + <field eval="12" name="method_period"/> + <field eval="3" name="method_number"/> + <field name="date" eval="time.strftime('%Y-01-01')"/> + <field name="name">Car Maintenance</field> + <field eval="30000.0" name="value"/> + <field name="category_id" ref="account_asset_category_sale_test0"/> + </record> + + <record id="account_asset_asset_a/c" model="account.asset.asset"> + <field eval="1" name="prorata"/> + <field name="state">open</field> + <field eval="1" name="method_period"/> + <field eval="12" name="method_number"/> + <field name="date" eval="time.strftime('%Y-01-01')"/> + <field name="name">Air Conditioner Maintenance Contract</field> + <field eval="1000.0" name="value"/> + <field name="category_id" ref="account_asset_category_sale1"/> + </record> + + + <!-- + Assets Tests + --> + <!-- + <record id="data_fiscalyear_plus1" model="account.fiscalyear"> + <field ref="base.main_company" name="company_id"/> + <field eval="'%s-01-01' %(datetime.now().year+1)" name="date_start"/> + <field eval="'%s-12-31' %(datetime.now().year+1)" name="date_stop"/> + <field name="name" eval="'Fiscal Year X %s' %(datetime.now().year+1)"/> + <field name="code" eval="'FY%s' %(datetime.now().year+1)"/> + </record> + <record id="data_fiscalyear_plus2" model="account.fiscalyear"> + <field ref="base.main_company" name="company_id"/> + <field eval="'%s-01-01' %(datetime.now().year+2)" name="date_start"/> + <field eval="'%s-12-31' %(datetime.now().year+2)" name="date_stop"/> + <field name="name" eval="'Fiscal Year X %s' %(datetime.now().year+2)"/> + <field name="code" eval="'FY%s' %(datetime.now().year+2)"/> + </record> + <record id="data_fiscalyear_plus3" model="account.fiscalyear"> + <field ref="base.main_company" name="company_id"/> + <field eval="'%s-01-01' %(datetime.now().year+3)" name="date_start"/> + <field eval="'%s-12-31' %(datetime.now().year+3)" name="date_stop"/> + <field name="name" eval="'Fiscal Year X %s' %(datetime.now().year+3)"/> + <field name="code" eval="'FY%s' %(datetime.now().year+3)"/> + </record> + <record id="data_fiscalyear_plus4" model="account.fiscalyear"> + <field ref="base.main_company" name="company_id"/> + <field eval="'%s-01-01' %(datetime.now().year+4)" name="date_start"/> + <field eval="'%s-12-31' %(datetime.now().year+4)" name="date_stop"/> + <field name="name" eval="'Fiscal Year X %s' %(datetime.now().year+4)"/> + <field name="code" eval="'FY%s' %(datetime.now().year+4)"/> + </record> + <record id="data_fiscalyear_plus5" model="account.fiscalyear"> + <field ref="base.main_company" name="company_id"/> + <field eval="'%s-01-01' %(datetime.now().year+5)" name="date_start"/> + <field eval="'%s-12-31' %(datetime.now().year+5)" name="date_stop"/> + <field name="name" eval="'Fiscal Year X %s' %(datetime.now().year+5)"/> + <field name="code" eval="'FY%s' %(datetime.now().year+5)"/> + </record> + --> </data> -</openerp> \ No newline at end of file +</odoo> diff --git a/addons/account_asset/test/account_asset_wizard.yml b/addons/account_asset/test/account_asset_wizard.yml deleted file mode 100644 index 91a85218406bfb14e1a3f2434b82a2ca806b2bdc..0000000000000000000000000000000000000000 --- a/addons/account_asset/test/account_asset_wizard.yml +++ /dev/null @@ -1,27 +0,0 @@ -- - I create a record to change the duration of asset for calculating depreciation. -- - !record {model: asset.modify, id: asset_modify_number_0, context: "{'active_id': ref('account_asset_asset_office_test0')}"}: - method_number: 10.0 -- - I change the duration. -- - !python {model: asset.modify}: | - context = {"active_id":ref('account_asset_asset_office_test0')} - self.modify(cr, uid, [ref("asset_modify_number_0")], context=context) -- - I check the proper depreciation lines created. -- - !assert {model: account.asset.asset, id: account_asset.account_asset_asset_office_test0}: - - method_number == len(depreciation_line_ids) -1 -- - I create a period to compute a asset on period. -- - !record {model: asset.depreciation.confirmation.wizard, id: asset_compute_period_0}: - {} -- - I compute a asset on period. -- - !python {model: asset.depreciation.confirmation.wizard}: | - context = {"active_ids": [ref("account_asset.account_asset_asset_office_test0")], "active_id":ref('account_asset.account_asset_asset_office_test0')} - self.asset_compute(cr, uid, [ref("asset_compute_period_0")], context=context) diff --git a/addons/account_asset/tests/__init__.py b/addons/account_asset/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..054eb7df4fe79a804745a63a74c0f267f75e3a94 --- /dev/null +++ b/addons/account_asset/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# +############################################################################## + +from . import test_account_asset diff --git a/addons/account_asset/tests/test_account_asset.py b/addons/account_asset/tests/test_account_asset.py new file mode 100644 index 0000000000000000000000000000000000000000..c3c582e9507a1e4d0f7bfa7ac2e3624b6c71ee74 --- /dev/null +++ b/addons/account_asset/tests/test_account_asset.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +from openerp import tools +from openerp.tests import common +from openerp.modules.module import get_module_resource + + +class TestAccountAsset(common.TransactionCase): + + def _load(self, module, *args): + tools.convert_file(self.cr, 'account_asset', + get_module_resource(module, *args), + {}, 'init', False, 'test', self.registry._assertion_report) + + def test_00_account_asset_asset(self): + self._load('account', 'test', 'account_minimal_test.xml') + self._load('account_asset', 'test', 'account_asset_demo_test.xml') + + # self.browse_ref("account_asset.data_fiscalyear_plus1").create_period() + # self.browse_ref("account_asset.data_fiscalyear_plus2").create_period() + # self.browse_ref("account_asset.data_fiscalyear_plus3").create_period() + # self.browse_ref("account_asset.data_fiscalyear_plus4").create_period() + # self.browse_ref("account_asset.data_fiscalyear_plus5").create_period() + + # In order to test the process of Account Asset, I perform a action to confirm Account Asset. + self.browse_ref("account_asset.account_asset_asset_vehicles0").validate() + + # I check Asset is now in Open state. + self.assertEqual(self.browse_ref("account_asset.account_asset_asset_vehicles0").state, 'open', + 'Asset should be in Open state') + + # I compute depreciation lines for asset of CEOs Car. + self.browse_ref("account_asset.account_asset_asset_vehicles0").compute_depreciation_board() + value = self.browse_ref("account_asset.account_asset_asset_vehicles0") + self.assertEqual(value.method_number, len(value.depreciation_line_ids), + 'Depreciation lines not created correctly') + + # I create account move for all depreciation lines. + ids = self.env['account.asset.depreciation.line'].search([('asset_id', '=', self.ref('account_asset.account_asset_asset_vehicles0'))]) + for line in ids: + line.create_move() + + # I check the move line is created. + asset = self.env['account.asset.asset'].browse([self.ref("account_asset.account_asset_asset_vehicles0")])[0] + self.assertEqual(len(asset.depreciation_line_ids), len(asset.account_move_line_ids), + 'Move lines not created correctly') + + # I Check that After creating all the moves of depreciation lines the state "Close". + self.assertEqual(self.browse_ref("account_asset.account_asset_asset_vehicles0").state, 'close', + 'State of asset should be close') + + invoice = self.env['account.invoice'].create({ + 'partner_id': self.ref("base.res_partner_12"), + 'account_id': self.ref("account_asset.a_sale"), + }) + self.env['account.invoice.line'].create({ + 'invoice_id': invoice.id, + 'account_id': self.ref("account_asset.a_sale"), + 'name': 'Insurance claim', + 'price_unit': 450, + 'quantity': 1, + 'asset_category_id': self.ref("account_asset.account_asset_category_sale1"), + }) + invoice.signal_workflow('invoice_open') + + from datetime import datetime + from dateutil.relativedelta import relativedelta + line_obj = self.env['account.asset.depreciation.line'] + recognition_ids = self.env['account.asset.asset'].search([('code', '=', invoice.number)]) + self.assertTrue(recognition_ids, + 'Revenue recognition has been not created from invoice.') + + # I confirm revenue recognition. + for asset in recognition_ids: + asset.validate() + recognition = recognition_ids[0] + first_invoice_line = invoice.invoice_line_ids[0] + self.assertTrue(recognition.state == 'open', + 'Recognition should be in Open state') + self.assertEqual(recognition.value, first_invoice_line.price_subtotal, + 'Recognition value is not same as invoice line.') + + # I post installment lines. + line_ids = [rec for rec in recognition.depreciation_line_ids] + for line in line_ids: + line.create_move() + + # I check that move line is created from posted installment lines. + self.assertEqual(len(recognition.depreciation_line_ids), len(recognition.account_move_line_ids), + 'Move lines not created correctly.') + + # I check data in move line and installment line. + first_installment_line = recognition.depreciation_line_ids[0] + first_move_line = recognition.account_move_line_ids[0] + self.assertEqual(first_installment_line.amount, first_move_line.credit, + 'First installment line amount is incorrect.') + remaining_value = recognition.value - first_installment_line.amount + self.assertEqual(first_installment_line.remaining_value, recognition.value - first_installment_line.amount, + 'Remaining value is incorrect.') + + # I check next installment date. + last_installment_date = datetime.strptime(first_installment_line.depreciation_date, '%Y-%m-%d') + installment_date = (last_installment_date+relativedelta(months=+recognition.method_period)) + self.assertEqual(recognition.depreciation_line_ids[1].depreciation_date, str(installment_date.date()), + 'Installment date is incorrect.') + + # WIZARD + # I create a record to change the duration of asset for calculating depreciation. + + account_asset_asset_office0 = self.browse_ref('account_asset.account_asset_asset_office0') + asset_modify_number_0 = self.env['asset.modify'].create({ + 'name': 'Test reason', + 'method_number': 10.0, + }).with_context({'active_id': account_asset_asset_office0.id}) + # I change the duration. + asset_modify_number_0.with_context({'active_id': account_asset_asset_office0.id}).modify() + + # I check the proper depreciation lines created. + self.assertEqual(account_asset_asset_office0.method_number, len(account_asset_asset_office0.depreciation_line_ids) - 1) + # I compute a asset on period. + + context = { + "active_ids": [self.ref("account_asset.menu_asset_depreciation_confirmation_wizard")], + "active_id": self.ref('account_asset.menu_asset_depreciation_confirmation_wizard'), + 'type': 'sale' + } + asset_compute_period_0 = self.env['asset.depreciation.confirmation.wizard'].create({}) + asset_compute_period_0.with_context(context).asset_compute() diff --git a/addons/account_asset/views/account_asset.xml b/addons/account_asset/views/account_asset.xml new file mode 100644 index 0000000000000000000000000000000000000000..198e95172d4474481d248df1c53a2ed020affe49 --- /dev/null +++ b/addons/account_asset/views/account_asset.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_backend" name="account assets" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/account_asset/static/src/js/account_asset.js"></script> + </xpath> + </template> +</odoo> + + diff --git a/addons/account_asset/views/account_asset_invoice_view.xml b/addons/account_asset/views/account_asset_invoice_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4207df4dd6d1496c24ddb597b639220e3c8a185 --- /dev/null +++ b/addons/account_asset/views/account_asset_invoice_view.xml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<odoo> + + <record model="ir.ui.view" id="view_account_invoice_asset_form"> + <field name="name">account.invoice.line.form</field> + <field name="model">account.invoice.line</field> + <field name="inherit_id" ref="account.view_invoice_line_form"/> + <field name="arch" type="xml"> + <field name="account_id" position="after"> + <field name="asset_category_id" domain="[('type','=','sale')]" string="Revenue Recognition Category"/> + </field> + </field> + </record> + + <!-- Add "Asset Category" to supplier invoices --> + <record model="ir.ui.view" id="view_invoice_asset_category"> + <field name="name">account.invoice.supplier.form</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.invoice_supplier_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']" position="before"> + <field string="Asset Category" name="asset_category_id" domain="[('type','=','purchase')]" context="{'default_type':'purchase'}"/> + </xpath> + </field> + </record> + + <!-- Add "Asset Category" to customer invoices --> + <record model="ir.ui.view" id="view_invoice_revenue_recognition_category"> + <field name="name">account.invoice.form</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.invoice_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='quantity']" position="before"> + <field string="Deferred Revenue Category" name="asset_category_id" domain="[('type','=','sale')]" context="{'default_type':'sale'}"/> + </xpath> + </field> + </record> + +</odoo> diff --git a/addons/account_asset/account_asset_view.xml b/addons/account_asset/views/account_asset_view.xml similarity index 55% rename from addons/account_asset/account_asset_view.xml rename to addons/account_asset/views/account_asset_view.xml index 8a2ab04ce6a146460c5de5a8d40edce69979fd01..1062a574243a08568f14711e6b602ca32b70682b 100644 --- a/addons/account_asset/account_asset_view.xml +++ b/addons/account_asset/views/account_asset_view.xml @@ -1,5 +1,4 @@ -<openerp> -<data> +<odoo> <!-- Asset Category @@ -13,25 +12,39 @@ <group> <group> <field name="name"/> + <field name="type" attrs="{'invisible': 1}"/> <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> </group> <group> <field name="journal_id"/> - <field name="account_asset_id" on_change="onchange_account_asset(account_asset_id)"/> - <field name="account_depreciation_id"/> - <field name="account_expense_depreciation_id"/> + <div> + <label for="account_asset_id" string="Asset Account" attrs="{'invisible': [('type','!=','purchase')]}"/> + <label for="account_asset_id" string="Deferred Revenue Account" attrs="{'invisible': [('type','!=','sale')]}"/> + </div> + <field name="account_asset_id" nolabel="1" attrs="{'invisible': [('type','=', False)]}"/> + <div> + <label for="account_depreciation_id" string="Depreciation Account" attrs="{'invisible': [('type','!=','purchase')]}"/> + <label for="account_depreciation_id" string="Income Account" attrs="{'invisible': [('type','!=','sale')]}"/> + </div> + <field name="account_depreciation_id" nolabel="1"/> </group> - <group string="Depreciation Dates"> - <field name="method_time"/> - <field name="method_number" attrs="{'invisible':[('method_time','=','end')], 'required':[('method_time','=','number')]}"/> - <field name="method_period"/> - <field name="method_end" attrs="{'required': [('method_time','=','end')], 'invisible':[('method_time','=','number')]}"/> + <group> + <separator string="Periodicity" colspan="2"/> + <field name="method_time" string="Time Method Based On" widget="radio" attrs="{'invisible': [('type','!=','purchase')]}"/> + <field name="method_number" string="Number of Entries" attrs="{'invisible':['|',('method_time','!=','number'),'&',('type','=', False)], 'required':[('method_time','=','number')]}"/> + <label for="method_period" string="Every"/> + <div> + <field name="method_period" nolabel="1" attrs="{'invisible': [('type','=', False)]}" class="oe_inline"/> + months + </div> + <field name="method_end" attrs="{'required': [('method_time','=','end')], 'invisible':[('method_time','!=','end')]}"/> + <field name="open_asset"/> </group> - <group string="Depreciation Method"> + <group attrs="{'invisible': [('type','=','sale')]}"> + <separator string="Depreciation Method" colspan="2"/> <field name="method" widget="radio"/> <field name="method_progress_factor" attrs="{'invisible':[('method','=','linear')], 'required':[('method','=','degressive')]}"/> <field name="prorata"/> - <field name="open_asset"/> </group> <group groups="analytic.group_analytic_accounting" string="Analytic Information"> <field name="account_analytic_id"/> @@ -61,25 +74,28 @@ <field name="model">account.asset.category</field> <field name="arch" type="xml"> <search string="Search Asset Category"> - <field name="name" string="Asset Category"/> + <filter string="Sales" domain="[('type','=', 'sale')]" help="Deferred Revenues"/> + <filter string="Purchase" domain="[('type','=', 'purchase')]" help="Assets"/> + <field name="name" string="Category"/> <field name="journal_id"/> + <group expand="0" string="Group By..."> + <filter string="Type" domain="[]" context="{'group_by':'type'}"/> + </group> </search> </field> </record> - <!-- - Asset - --> - <record model="ir.ui.view" id="view_account_asset_asset_form"> <field name="name">account.asset.asset.form</field> <field name="model">account.asset.asset</field> <field name="arch" type="xml"> <form string="Asset"> <header> - <button name="validate" states="draft" string="Confirm Asset" type="object" class="oe_highlight"/> + <button name="validate" states="draft" string="Confirm" type="object" class="oe_highlight"/> + <button type="object" name="compute_depreciation_board" string="Compute Depreciation" states="draft"/> <button name="set_to_close" states="open" string="Set to Close" type="object" class="oe_highlight"/> <button name="set_to_draft" states="open" string="Set to Draft" type="object" /> + <button name="%(action_asset_modify)d" states="open" string="Modify Depreciation" type="action"/> <field name="state" widget="statusbar" statusbar_visible="draft,open"/> </header> <sheet> @@ -96,63 +112,46 @@ </div> <group> <group> - <field name="category_id" on_change="onchange_category_id(category_id)"/> + <field name="category_id" domain="[('type', '=', default_category_id_type)]" context="{'default_type': default_category_id_type}" help="Category of asset"/> <field name="code"/> + <field name="date" help="Date of asset"/> + <field name="type" invisible="1"/> </group> <group> - <field name="purchase_date"/> <field name="currency_id" groups="base.group_multi_currency"/> - <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company" on_change="onchange_company_id(company_id)"/> + <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> + <field name="value" widget="monetary" options="{'currency_field': 'currency_id'}" help="Gross value of asset"/> + <field name="salvage_value" widget="monetary" options="{'currency_field': 'currency_id'}"/> + <field name="value_residual" widget="monetary" options="{'currency_field': 'currency_id'}"/> + <field name="partner_id" string="Partner"/> + <field name="invoice_id" string="Invoice" domain="[('invoice_line_ids.asset_category_id.type','=','purchase')]"/> </group> </group> <notebook colspan="4"> - <page string="General"> - <group> - <group> - <field name="purchase_value" widget="monetary" options="{'currency_field': 'currency_id'}" on_change="onchange_purchase_salvage_value(purchase_value, salvage_value)"/> - <field name="salvage_value" widget="monetary" options="{'currency_field': 'currency_id'}" on_change="onchange_purchase_salvage_value(purchase_value, salvage_value)"/> - <field name="value_residual" widget="monetary" options="{'currency_field': 'currency_id'}"/> - <field name="partner_id"/> - </group> - <group> - <field name="method" widget="radio"/> - <field name="method_progress_factor" attrs="{'invisible':[('method','=','linear')], 'required':[('method','=','degressive')]}"/> - <label for="method_time"/> - <div> - <field name="method_time" on_change="onchange_method_time(method_time)" class="oe_inline"/> - <button name="%(action_asset_modify)d" states="open" string="Change Duration" type="action" icon="terp-stock_effects-object-colorize" class="oe_inline" colspan="1"/> - </div> - <field name="prorata" attrs="{'invisible': [('method_time','=','end')]}"/> - <field name="method_number" attrs="{'invisible':[('method_time','=','end')], 'required':[('method_time','=','number')]}"/> - <field name="method_period"/> - <field name="method_end" attrs="{'required': [('method_time','=','end')], 'invisible':[('method_time','=','number')]}"/> - </group> - </group> - </page> <page string="Depreciation Board"> - <field name="depreciation_line_ids" mode="tree"> - <tree string="Depreciation Lines" decoration-info="(move_check == False)"> + <field name="depreciation_line_ids" mode="tree" options="{'reload_whole_on_button': true}"> + <tree string="Depreciation Lines" decoration-info="(move_check == False)" create="false"> + <field name="sequence" string="Serial Number"/> <field name="depreciation_date"/> - <field name="sequence" invisible="1"/> <field name="depreciated_value" readonly="1"/> <field name="amount" widget="monetary"/> <field name="remaining_value" readonly="1" widget="monetary"/> - <field name="move_check"/> + <field name="move_check" invisible="1"/> <field name="parent_state" invisible="1"/> - <button name="create_move" attrs="{'invisible':['|',('move_check','!=',False),('parent_state','!=','open')]}" icon="gtk-execute" string="Create Move" type="object"/> + <button name="create_move" type="object" widget="widgetonbutton"/> </tree> <form string="Depreciation Lines"> <group> <group> <field name="parent_state" invisible="1"/> <field name="name"/> - <field name="amount" widget="monetary"/> + <field name="sequence"/> <field name="move_id"/> <field name="move_check"/> <field name="parent_state" invisible="1"/> </group> <group> - <field name="sequence"/> + <field name="amount" widget="monetary"/> <field name="depreciation_date"/> <field name="depreciated_value"/> <field name="remaining_value"/> @@ -160,17 +159,33 @@ </group> </form> </field> - <button type="object" name="compute_depreciation_board" string="Compute" icon="terp-stock_format-scientific" colspan="2" attrs="{'invisible':[('state','=','close')]}"/> </page> <page string="History"> - <field name="account_move_line_ids" readonly="1"/> <field name="history_ids" readonly="1"/> </page> - <page string="Notes"> - <field name="note" placeholder="Add an internal note here..."/> - </page> </notebook> + <separator string="Depreciation Information" colspan="4"/> + <group> + <group> + <field name="method" widget="radio" attrs="{'invisible': [('type','=','sale')]}"/> + <field name="method_progress_factor" attrs="{'invisible':[('method','=','linear')], 'required':[('method','=','degressive')]}"/> + <label for="method_time"/> + <div> + <field name="method_time"/> + </div> + <field name="prorata" attrs="{'invisible': [('method_time','=','end')]}"/> + </group> + <group> + <field name="method_number" attrs="{'invisible':[('method_time','=','end')], 'required':[('method_time','=','number')]}"/> + <field name="method_period"/> + <field name="method_end" attrs="{'required': [('method_time','=','end')], 'invisible':[('method_time','=','number')]}"/> + </group> + </group> </sheet> + <div class="oe_chatter"> + <field name="message_follower_ids" widget="mail_followers"/> + <field name="message_ids" widget="mail_thread"/> + </div> </form> </field> </record> @@ -182,27 +197,9 @@ <tree string="Assets" decoration-info="(state == 'draft')" decoration-muted="(state == 'close')"> <field name="name"/> <field name="category_id"/> - <field name="purchase_date"/> + <field name="date"/> <field name="partner_id"/> - <field name="purchase_value"/> - <field name="value_residual" widget="monetary"/> - <field name="currency_id" groups="base.group_multi_currency"/> - <field name="company_id" groups="base.group_multi_company"/> - <field name="state"/> - </tree> - </field> - </record> - - <record model="ir.ui.view" id="view_account_asset_asset_hierarchy_tree"> - <field name="name">account.asset.asset.hierarchy</field> - <field name="model">account.asset.asset</field> - <field name="arch" type="xml"> - <tree string="Assets"> - <field name="name"/> - <field name="code"/> - <field name="category_id"/> - <field name="purchase_date"/> - <field name="purchase_value"/> + <field name="value"/> <field name="value_residual" widget="monetary"/> <field name="currency_id" groups="base.group_multi_currency"/> <field name="company_id" groups="base.group_multi_company"/> @@ -217,15 +214,34 @@ <field name="arch" type="xml"> <search string="Account Asset"> <field name="name" string="Asset"/> - <field name="purchase_date"/> - <filter icon="terp-check" string="Current" domain="[('state','in', ('draft','open'))]" help="Assets in draft and open states"/> - <filter icon="terp-dialog-close" string="Closed" domain="[('state','=', 'close')]" help="Assets in closed state"/> + <field name="date"/> + <filter string="Current" domain="[('state','in', ('draft','open'))]" help="Assets in draft and open states"/> + <filter string="Closed" domain="[('state','=', 'close')]" help="Assets in closed state"/> <field name="category_id"/> <field name="partner_id" filter_domain="[('partner_id','child_of',self)]"/> + <group expand="0" string="Group By..."> + <filter string="Month" domain="[]" context="{'group_by':'date'}"/> + <filter string="Category" domain="[]" context="{'group_by':'category_id'}"/> + </group> </search> </field> </record> + <!-- Product Template --> + <record id="view_product_template_form_inherit" model="ir.ui.view"> + <field name="name">Product Template (form)</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_form_view"/> + <field name="arch" type="xml"> + <xpath expr="//group[@name='properties']" position="before"> + <group name="asset types"> + <field name="asset_category_id" domain="[('type','=','purchase')]" context="{'default_type':'purchase'}"/> + <field name="deferred_revenue_category_id" domain="[('type','=','sale')]" context="{'default_type':'sale'}"/> + </group> + </xpath> + </field> + </record> + <!-- Asset History --> @@ -269,15 +285,7 @@ </tree> </field> </record> - - <record model="ir.actions.act_window" id="action_account_asset_asset_tree"> - <field name="name">Asset Hierarchy</field> - <field name="res_model">account.asset.asset</field> - <field name="view_type">tree</field> - <field name="domain">[]</field> - <field name="view_id" ref="view_account_asset_asset_hierarchy_tree"/> - </record> - + <record id="view_account_move_line_form_inherit" model="ir.ui.view"> <field name="name">Journal Items (form)</field> <field name="model">account.move.line</field> @@ -300,26 +308,51 @@ </field> </record> + <menuitem id="menu_finance_assets" name="Assets and Revenues" parent="account.menu_finance_entries" sequence="9" groups="account.group_account_user"/> <record model="ir.actions.act_window" id="action_account_asset_asset_form"> <field name="name">Assets</field> <field name="res_model">account.asset.asset</field> + <field name="domain">[('category_id.type', '=', 'purchase')]</field> + <field name="context">{'default_type': 'purchase', 'default_category_id_type': 'purchase'}</field> + </record> + + <!-- Deferred Revenues Action --> + + <record model="ir.actions.act_window" id="action_account_revenue_form"> + <field name="name">Deferred Revenues</field> + <field name="res_model">account.asset.asset</field> <field name="view_type">form</field> - <field name="view_id" ref="view_account_asset_asset_tree"/> - <field name="search_view_id" ref="view_account_asset_search"/> + <field name="domain">[('category_id.type', '=', 'sale')]</field> + <field name="context">{'default_type': 'sale', 'default_category_id_type': 'sale'}</field> </record> - <menuitem parent="account.menu_finance_entries" id="menu_action_account_asset_asset_form" action="action_account_asset_asset_form" name="Assets" groups="account.group_account_user" sequence="50"/> + <menuitem parent="menu_finance_assets" id="menu_revenue_recognition" action="action_account_revenue_form" sequence="102"/> - <act_window id="act_entries_open" name="Entries" res_model="account.move.line" src_model="account.asset.asset" context="{'search_default_asset_id': [active_id], 'default_asset_id': active_id}"/> + <menuitem parent="menu_finance_assets" id="menu_action_account_asset_asset_form" action="action_account_asset_asset_form" sequence="104"/> - <record model="ir.actions.act_window" id="action_account_asset_asset_list_normal"> - <field name="name">Asset Categories</field> + + <!-- Configuration --> + + <menuitem id="menu_finance_config_assets" name="Assets and Revenues" parent="account.menu_finance_configuration" sequence="25"/> + + <record model="ir.actions.act_window" id="action_account_asset_asset_list_normal_sale"> + <field name="name">Asset Types</field> + <field name="res_model">account.asset.category</field> + <field name="domain">[('type', '=', 'purchase')]</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="context">{'default_type': 'purchase'}</field> + </record> + <record model="ir.actions.act_window" id="action_account_asset_asset_list_normal_purchase"> + <field name="name">Deferred Revenues Types</field> <field name="res_model">account.asset.category</field> + <field name="domain">[('type', '=', 'sale')]</field> <field name="view_type">form</field> <field name="view_mode">tree,form</field> + <field name="context">{'default_type': 'sale'}</field> </record> - <menuitem parent="account.account_management_menu" id="menu_action_account_asset_asset_list_normal" action="action_account_asset_asset_list_normal" sequence="2"/> + <menuitem parent="account.account_management_menu" id="menu_action_account_asset_asset_list_normal_sale" action="action_account_asset_asset_list_normal_sale" sequence="1"/> + <menuitem parent="account.account_management_menu" id="menu_action_account_asset_asset_list_normal_purchase" action="action_account_asset_asset_list_normal_purchase" sequence="2"/> -</data> -</openerp> +</odoo> diff --git a/addons/account_asset/wizard/__init__.py b/addons/account_asset/wizard/__init__.py index e1ee90f90c2c43ceeaf5d478fa8cac3752576b82..0425aadb5d8e844016cf459cdc7c1f35bf1ca2cc 100644 --- a/addons/account_asset/wizard/__init__.py +++ b/addons/account_asset/wizard/__init__.py @@ -1,23 +1,4 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## +# -*- coding: utf-8 -*- import account_asset_change_duration import wizard_asset_compute diff --git a/addons/account_asset/wizard/account_asset_change_duration.py b/addons/account_asset/wizard/account_asset_change_duration.py index 35160c026ffc0b77574eaff5b427a83f38d36e9d..55c1b3d3f7e9a89e55d18a2c15317cb9e45b3c79 100644 --- a/addons/account_asset/wizard/account_asset_change_duration.py +++ b/addons/account_asset/wizard/account_asset_change_duration.py @@ -1,63 +1,51 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## -import time +# -*- coding: utf-8 -*- from lxml import etree -from openerp.osv import fields, osv +from openerp import api, fields, models +from openerp.osv.orm import setup_modifiers -class asset_modify(osv.osv_memory): + +class AssetModify(models.TransientModel): _name = 'asset.modify' _description = 'Modify Asset' - def _get_asset_method_time(self, cr, uid, ids, field_name, arg, context=None): - if ids and len(ids) == 1 and context.get('active_id'): - asset = self.pool['account.asset.asset'].browse(cr, uid, context.get('active_id'), context=context) - return {ids[0]: asset.method_time} - else: - return dict.fromkeys(ids, False) + name = fields.Char(string='Reason', required=True) + method_number = fields.Integer(string='Number of Depreciations', required=True) + method_period = fields.Integer(string='Period Length') + method_end = fields.Date(string='Ending date') + note = fields.Text(string='Notes') + asset_method_time = fields.Char(compute='_get_asset_method_time', string='Asset Method Time', readonly=True) + + @api.one + def _get_asset_method_time(self): + if self.env.context.get('active_id'): + asset = self.env['account.asset.asset'].browse(self.env.context.get('active_id')) + self.asset_method_time = asset.method_time - _columns = { - 'name': fields.char('Reason', required=True), - 'method_number': fields.integer('Number of Depreciations', required=True), - 'method_period': fields.integer('Period Length'), - 'method_end': fields.date('Ending date'), - 'note': fields.text('Notes'), - 'asset_method_time': fields.function(_get_asset_method_time, type='char', string='Asset Method Time', readonly=True), - } + @api.model + def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): + result = super(AssetModify, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=submenu) + asset_id = self.env.context.get('active_id') + active_model = self.env.context.get('active_model') + if active_model == 'account.asset.asset' and asset_id: + asset = self.env['account.asset.asset'].browse(asset_id) + doc = etree.XML(result['arch']) + if asset.method_time == 'number' and doc.xpath("//field[@name='method_end']"): + node = doc.xpath("//field[@name='method_end']")[0] + node.set('invisible', '1') + setup_modifiers(node, result['fields']['method_end']) + elif asset.method_time == 'end' and doc.xpath("//field[@name='method_number']"): + node = doc.xpath("//field[@name='method_number']")[0] + node.set('invisible', '1') + setup_modifiers(node, result['fields']['method_number']) + result['arch'] = etree.tostring(doc) + return result - def default_get(self, cr, uid, fields, context=None): - """ To get default values for the object. - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param fields: List of fields for which we want default values - @param context: A standard dictionary - @return: A dictionary which of fields with values. - """ - if not context: - context = {} - asset_obj = self.pool.get('account.asset.asset') - res = super(asset_modify, self).default_get(cr, uid, fields, context=context) - asset_id = context.get('active_id', False) - asset = asset_obj.browse(cr, uid, asset_id, context=context) + @api.model + def default_get(self, fields): + res = super(AssetModify, self).default_get(fields) + asset_id = self.env.context.get('active_id') + asset = self.env['account.asset.asset'].browse(asset_id) if 'name' in fields: res.update({'name': asset.name}) if 'method_number' in fields and asset.method_time == 'number': @@ -66,44 +54,34 @@ class asset_modify(osv.osv_memory): res.update({'method_period': asset.method_period}) if 'method_end' in fields and asset.method_time == 'end': res.update({'method_end': asset.method_end}) - if context.get('active_id'): - res['asset_method_time'] = self._get_asset_method_time(cr, uid, [0], 'asset_method_time', [], context=context)[0] + if self.env.context.get('active_id'): + res['asset_method_time'] = self._get_asset_method_time() return res - - def modify(self, cr, uid, ids, context=None): + + @api.multi + def modify(self): """ Modifies the duration of asset for calculating depreciation and maintains the history of old values. - @param self: The object pointer. - @param cr: A database cursor - @param uid: ID of the user currently logged in - @param ids: List of Ids - @param context: A standard dictionary - @return: Close the wizard. - """ - if not context: - context = {} - asset_obj = self.pool.get('account.asset.asset') - history_obj = self.pool.get('account.asset.history') - asset_id = context.get('active_id', False) - asset = asset_obj.browse(cr, uid, asset_id, context=context) - data = self.browse(cr, uid, ids[0], context=context) + """ + asset_id = self.env.context.get('active_id', False) + asset = self.env['account.asset.asset'].browse(asset_id) history_vals = { 'asset_id': asset_id, - 'name': data.name, + 'name': self.name, 'method_time': asset.method_time, 'method_number': asset.method_number, 'method_period': asset.method_period, 'method_end': asset.method_end, - 'user_id': uid, - 'date': time.strftime('%Y-%m-%d'), - 'note': data.note, + 'user_id': self.env.uid, + 'date': fields.Date.context_today(self), + 'note': self.note, } - history_obj.create(cr, uid, history_vals, context=context) + self.env['account.asset.history'].create(history_vals) asset_vals = { - 'method_number': data.method_number, - 'method_period': data.method_period, - 'method_end': data.method_end, + 'method_number': self.method_number, + 'method_period': self.method_period, + 'method_end': self.method_end, } - asset_obj.write(cr, uid, [asset_id], asset_vals, context=context) - asset_obj.compute_depreciation_board(cr, uid, [asset_id], context=context) + asset.write(asset_vals) + asset.compute_depreciation_board() return {'type': 'ir.actions.act_window_close'} diff --git a/addons/account_asset/wizard/account_asset_change_duration_view.xml b/addons/account_asset/wizard/account_asset_change_duration_view.xml index daa736fb80435ed88abf3fe11f3cdfd980f0030c..46ebac2a8e1fb95492134afd45574f278ed729e4 100644 --- a/addons/account_asset/wizard/account_asset_change_duration_view.xml +++ b/addons/account_asset/wizard/account_asset_change_duration_view.xml @@ -1,46 +1,44 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> +<odoo> - <record model="ir.ui.view" id="asset_modify_form"> - <field name="name">wizard.asset.modify.form</field> - <field name="model">asset.modify</field> - <field name="arch" type="xml"> - <form string="Modify Asset"> - <field name="asset_method_time" invisible="1"/> - <group string="Asset Durations to Modify" col="4"> - <group colspan="2" col="2"> - <field name="name"/> - <field name="method_number" attrs="{'invisible': [('asset_method_time', '=', 'end')]}"/> - </group> - <group colspan="2" col="2"> - <field name="method_end" attrs="{'invisible': [('asset_method_time', '=', 'number')]}"/> - <label for="method_period"/> - <div> - <field name="method_period" class="oe_inline"/> months - </div> - </group> + <record model="ir.ui.view" id="asset_modify_form"> + <field name="name">wizard.asset.modify.form</field> + <field name="model">asset.modify</field> + <field name="arch" type="xml"> + <form string="Modify Asset"> + <field name="asset_method_time" invisible="1"/> + <group string="Asset Durations to Modify" col="4"> + <group colspan="2" col="2"> + <field name="name"/> + <field name="method_number" attrs="{'invisible': [('asset_method_time', '=', 'end')]}"/> </group> - <separator string="Notes"/> - <field name="note"/> - <footer> - <button name="modify" string="Modify" type="object" class="oe_highlight"/> - or - <button string="Cancel" class="oe_link" special="cancel"/> - </footer> - </form> - </field> - </record> - - <record id="action_asset_modify" model="ir.actions.act_window"> - <field name="name">Modify Asset</field> - <field name="res_model">asset.modify</field> - <field name="type">ir.actions.act_window</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="view_id" ref="asset_modify_form"/> - <field name="target">new</field> - </record> + <group colspan="2" col="2"> + <field name="method_end" attrs="{'invisible': [('asset_method_time', '=', 'number')]}"/> + <label for="method_period"/> + <div> + <field name="method_period" class="oe_inline"/> months + </div> + </group> + </group> + <separator string="Notes"/> + <field name="note"/> + <footer> + <button name="modify" string="Modify" type="object" class="oe_highlight"/> + or + <button string="Cancel" class="oe_link" special="cancel"/> + </footer> + </form> + </field> + </record> + + <record id="action_asset_modify" model="ir.actions.act_window"> + <field name="name">Modify Asset</field> + <field name="res_model">asset.modify</field> + <field name="type">ir.actions.act_window</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="asset_modify_form"/> + <field name="target">new</field> + </record> - </data> -</openerp> +</odoo> diff --git a/addons/account_asset/wizard/wizard_asset_compute.py b/addons/account_asset/wizard/wizard_asset_compute.py index 0a16f8bc2df92e93dce771b28633a62f005a624f..0a3711a7f764bf9ec0b544520c308bc13fc750f3 100644 --- a/addons/account_asset/wizard/wizard_asset_compute.py +++ b/addons/account_asset/wizard/wizard_asset_compute.py @@ -1,50 +1,29 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). -# -# 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 <http://www.gnu.org/licenses/>. -# -############################################################################## +# -*- coding: utf-8 -*- +from openerp import api, fields, models, _ -from openerp.osv import fields, osv -from openerp.tools.translate import _ -class asset_depreciation_confirmation_wizard(osv.osv_memory): +class AssetDepreciationConfirmationWizard(models.TransientModel): _name = "asset.depreciation.confirmation.wizard" _description = "asset.depreciation.confirmation.wizard" - _columns = { - 'date': fields.date('Account Date', required=True, help="Choose the period for which you want to automatically post the depreciation lines of running assets"), - } - - _defaults = { - 'date': fields.date.context_today, - } - def asset_compute(self, cr, uid, ids, context): - ass_obj = self.pool.get('account.asset.asset') - asset_ids = context.get('active_ids', False) - data = self.browse(cr, uid, ids, context=context) - date = data[0].date - created_move_ids = ass_obj._compute_entries(cr, uid, asset_ids, date, context=context) + date = fields.Date('Account Date', required=True, help="Choose the period for which you want to automatically post the depreciation lines of running assets", default=fields.Date.context_today) + + @api.multi + def asset_compute(self): + self.ensure_one() + context = self._context + assets = self.env['account.asset.asset'].search([('state', '=', 'open'), ('category_id.type', '=', context.get('asset_type'))]) + created_move_ids = assets._compute_entries(self.date) + if context.get('asset_type') == 'purchase': + title = _('Created Asset Moves') + else: + title = _('Created Revenue Moves') return { - 'name': _('Created Asset Moves'), + 'name': title, 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'view_id': False, - 'domain': "[('id','in',["+','.join(map(str,created_move_ids))+"])]", + 'domain': "[('id','in',["+','.join(map(str, created_move_ids))+"])]", 'type': 'ir.actions.act_window', } diff --git a/addons/account_asset/wizard/wizard_asset_compute_view.xml b/addons/account_asset/wizard/wizard_asset_compute_view.xml index a76719ebf8ad0e71a0ed5a721f47a340793d5dce..8e31c102bd7c397182e4d01f7127d727931728f1 100644 --- a/addons/account_asset/wizard/wizard_asset_compute_view.xml +++ b/addons/account_asset/wizard/wizard_asset_compute_view.xml @@ -1,40 +1,54 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - <record id="view_asset_depreciation_confirmation_wizard" model="ir.ui.view"> - <field name="name">asset.depreciation.confirmation.wizard</field> - <field name="model">asset.depreciation.confirmation.wizard</field> - <field name="arch" type="xml"> - <form string="Compute Asset"> - <group> - <field name="date"/> - </group> - <footer> - <button string="Compute" name="asset_compute" type="object" class="oe_highlight"/> - or - <button string="Cancel" class="oe_link" special="cancel"/> - </footer> - </form> - </field> - </record> +<odoo> + <record id="view_asset_depreciation_confirmation_wizard" model="ir.ui.view"> + <field name="name">asset.depreciation.confirmation.wizard</field> + <field name="model">asset.depreciation.confirmation.wizard</field> + <field name="arch" type="xml"> + <form string="Compute Asset"> + <div> + <p> + This wizard will post installment/depreciation lines for the selected month.<br/> + This will generate journal entries for all related installment lines on this period of asset/revenue recognition as well. + </p> + </div> + <group> + <field name="date"/> + </group> + <footer> + <button string="Generate Entries" name="asset_compute" type="object" class="oe_highlight"/> + or + <button string="Cancel" class="oe_link" special="cancel"/> + </footer> + </form> + </field> + </record> - <record id="action_asset_depreciation_confirmation_wizard" model="ir.actions.act_window"> - <field name="name">Generate Depreciation Entries</field> - <field name="res_model">asset.depreciation.confirmation.wizard</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="view_id" ref="view_asset_depreciation_confirmation_wizard"/> - <field name="target">new</field> - </record> + <record id="action_asset_depreciation_confirmation_wizard" model="ir.actions.act_window"> + <field name="name">Post Depreciation Lines</field> + <field name="res_model">asset.depreciation.confirmation.wizard</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="view_asset_depreciation_confirmation_wizard"/> + <field name="target">new</field> + <field name="context">{'asset_type': 'purchase'}</field> + </record> - <record model="ir.values" id="action_asset_depreciation_confirmation_wizard_multi"> - <field name="model_id" ref="account_asset.model_account_asset_asset" /> - <field name="name">Generate Depreciation Entries</field> - <field name="key2">client_action_multi</field> - <field name="value" eval="'ir.actions.act_window,' +str(ref('action_asset_depreciation_confirmation_wizard'))" /> - <field name="key">action</field> - <field name="model">account.asset.asset</field> - </record> + <record id="action_recognition_depreciation_confirmation_wizard" model="ir.actions.act_window"> + <field name="name">Post Installment Lines</field> + <field name="res_model">asset.depreciation.confirmation.wizard</field> + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="view_asset_depreciation_confirmation_wizard"/> + <field name="target">new</field> + <field name="context">{'asset_type': 'sale'}</field> + </record> - </data> -</openerp> + <menuitem name="Deferred Revenue Entries" action="action_recognition_depreciation_confirmation_wizard" + id="menu_recognition_depreciation_confirmation_wizard" + parent="account.menu_finance_payables" sequence="2"/> + + <menuitem name="Assets Entries" action="action_asset_depreciation_confirmation_wizard" + id="menu_asset_depreciation_confirmation_wizard" + parent="account.menu_finance_receivables" sequence="1"/> + +</odoo> diff --git a/addons/account_cancel/account_cancel_view.xml b/addons/account_cancel/account_cancel_view.xml index 668c1053e08a00683cb2cb8d5c18275ba0b1ea56..d64d3101c64bb4b96d5d0758c4bdcbb53ac7bcb0 100644 --- a/addons/account_cancel/account_cancel_view.xml +++ b/addons/account_cancel/account_cancel_view.xml @@ -39,6 +39,9 @@ <field name="model">account.bank.statement</field> <field name="inherit_id" ref="account.view_bank_statement_form"/> <field name="arch" type="xml"> + <field name="line_ids" position="attributes"> + <attribute name="options">{'reload_on_button': true}</attribute> + </field> <xpath expr="//field[@name='bank_account_id']" position="after"> <button name="button_cancel_reconciliation" attrs="{'invisible': [('journal_entry_ids', '=', [])]}" string="Cancel" type="object" icon="gtk-undo"/> </xpath> diff --git a/addons/account_test/report/account_test_report.py b/addons/account_test/report/account_test_report.py index a19e2f398af3b386ac4d74b8fb7aafce2471fade..208fec8152aced18041be68858e4ad452b200d98 100644 --- a/addons/account_test/report/account_test_report.py +++ b/addons/account_test/report/account_test_report.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import datetime from openerp import api, models, _ +from openerp.tools.safe_eval import safe_eval as eval # # Use period and Journal for selection or resources # @@ -38,7 +39,7 @@ class ReportAssertAccount(models.AbstractModel): 'result': None, # used to store the result of the test 'column_order': None, # used to choose the display order of columns (in case you are returning a list of dict) } - exec code_exec in localdict + eval(code_exec, localdict, mode="exec", nocopy=True) result = localdict['result'] column_order = localdict.get('column_order', None) diff --git a/addons/analytic_contract_hr_expense/analytic_contract_hr_expense.py b/addons/analytic_contract_hr_expense/analytic_contract_hr_expense.py index a9047db017a223644957a424839050240d2c8909..4254e605441e1922a21e9fe783e25177ab28f6f2 100644 --- a/addons/analytic_contract_hr_expense/analytic_contract_hr_expense.py +++ b/addons/analytic_contract_hr_expense/analytic_contract_hr_expense.py @@ -114,8 +114,8 @@ class account_analytic_account(osv.osv): digits=0), } - def on_change_template(self, cr, uid, id, template_id, date_start=False, context=None): - res = super(account_analytic_account, self).on_change_template(cr, uid, id, template_id, date_start=date_start, context=context) + def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None): + res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context) if template_id and 'value' in res: template = self.browse(cr, uid, template_id, context=context) res['value']['charge_expenses'] = template.charge_expenses diff --git a/addons/anonymization/anonymization.py b/addons/anonymization/anonymization.py index 35c6f98d06eb01bd69e8685416cfdddbb2fa6278..a114c7712ffb4cd2f4d508a05111015ecdb0a5ad 100644 --- a/addons/anonymization/anonymization.py +++ b/addons/anonymization/anonymization.py @@ -31,6 +31,7 @@ import random import datetime from openerp.osv import fields, osv from openerp.tools.translate import _ +from openerp.tools.safe_eval import safe_eval as eval from itertools import groupby from operator import itemgetter diff --git a/addons/auth_ldap/users_ldap.py b/addons/auth_ldap/users_ldap.py index 937e672ef8914caea6307627a37f889363007846..6d2040d83c44855b0167cb3d4ea4c6abf1e1e728 100644 --- a/addons/auth_ldap/users_ldap.py +++ b/addons/auth_ldap/users_ldap.py @@ -140,8 +140,8 @@ class CompanyLDAP(osv.osv): results = [] try: conn = self.connect(conf) - conn.simple_bind_s(conf['ldap_binddn'] or '', - conf['ldap_password'].encode('utf-8') or '') + ldap_password = conf['ldap_password'] or '' + conn.simple_bind_s(conf['ldap_binddn'] or '', ldap_password.encode('utf-8')) results = conn.search_st(conf['ldap_base'], ldap.SCOPE_SUBTREE, filter, retrieve_attributes, timeout=60) conn.unbind() diff --git a/addons/barcodes/static/src/js/barcode_parser.js b/addons/barcodes/static/src/js/barcode_parser.js index e1703ae62b469ca58aa547b873b9dde9e48bdced..970019fb1f40060f23c2525cb82c8889758a1146 100644 --- a/addons/barcodes/static/src/js/barcode_parser.js +++ b/addons/barcodes/static/src/js/barcode_parser.js @@ -158,7 +158,14 @@ var BarcodeParser = Class.extend({ .replace("\.","."); } - match['match'] = match['base_code'].substr(0,base_pattern.length).match(base_pattern); + if (base_pattern[0] !== '^') { + base_pattern = "^" + base_pattern; + } + if (base_pattern[base_pattern.length -1] !== '$') { + base_pattern = base_pattern + ".*$"; + } + match.match = match.base_code.match(base_pattern); + return match; }, diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py index dae5d0e73965c360fa02c6347ed7e83e955d807c..3e3a57ef3696b703f8e44ba7864df108e0bd24c2 100644 --- a/addons/base_action_rule/base_action_rule.py +++ b/addons/base_action_rule/base_action_rule.py @@ -27,6 +27,7 @@ import openerp from openerp import SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.safe_eval import safe_eval as eval _logger = logging.getLogger(__name__) diff --git a/addons/base_import/models.py b/addons/base_import/models.py index 9778391521aa6b3ba4235c34501b772e6fc0a23d..784ae7f49f1f7add978ab7ff459eaa0fa0a459f8 100644 --- a/addons/base_import/models.py +++ b/addons/base_import/models.py @@ -158,12 +158,19 @@ class ir_import(orm.TransientModel): all the fields to traverse :rtype: list(Field) """ + string_match = None for field in fields: # FIXME: should match all translations & original # TODO: use string distance (levenshtein? hamming?) - if header.lower() == field['name'].lower() \ - or header.lower() == field['string'].lower(): + if header.lower() == field['name'].lower(): return [field] + if header.lower() == field['string'].lower(): + # matching string are not reliable way because + # strings have no unique constraint + string_match = field + if string_match: + # this behavior is only applied if there is no matching field['name'] + return [string_match] if '/' not in header: return [] diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index 74157cfb226345a8e167b99e7a2fe62700e3d2be..84fd9a24f58dc62418a20723320fdb391712135d 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -480,13 +480,13 @@ class calendar_alarm_manager(osv.AbstractModel): def get_next_notif(self, cr, uid, context=None): ajax_check_every_seconds = 300 - partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id + partner = self.pool['res.users'].read(cr, SUPERUSER_ID, uid, ['partner_id', 'calendar_last_notif_ack'], context=context) all_notif = [] if not partner: return [] - all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context) + all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner['partner_id'][0], mail=False, context=context) for event in all_events: # .values() max_delta = all_events[event]['max_duration'] @@ -496,7 +496,7 @@ class calendar_alarm_manager(osv.AbstractModel): LastFound = False for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context): in_date_format = one_date.replace(tzinfo=None) - LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context) + LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner['calendar_last_notif_ack'], mail=False, context=context) if LastFound: for alert in LastFound: all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context)) @@ -506,7 +506,7 @@ class calendar_alarm_manager(osv.AbstractModel): break else: in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT) - LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context) + LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner['calendar_last_notif_ack'], mail=False, context=context) if LastFound: for alert in LastFound: all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context)) @@ -586,7 +586,7 @@ class calendar_alarm(osv.Model): def _update_cron(self, cr, uid, context=None): try: cron = self.pool['ir.model.data'].get_object( - cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context) + cr, SUPERUSER_ID, 'calendar', 'ir_cron_scheduler_alarm', context=context) except ValueError: return False return cron.toggle(model=self._name, domain=[('type', '=', 'email')]) @@ -702,6 +702,9 @@ class calendar_event(osv.Model): val = pytz.UTC.localize(val) return val.astimezone(timezone) + if context is None: + context = {} + timezone = pytz.timezone(context.get('tz') or 'UTC') startdate = pytz.UTC.localize(datetime.strptime(event.start, DEFAULT_SERVER_DATETIME_FORMAT)) # Add "+hh:mm" timezone if not startdate: diff --git a/addons/crm/calendar_event_menu.xml b/addons/crm/calendar_event_menu.xml index aec88a5748ed10f4343174aa5c54bf2d2127d94d..eb3889f0857ebb1e62c6f155c58cf5574560b424 100644 --- a/addons/crm/calendar_event_menu.xml +++ b/addons/crm/calendar_event_menu.xml @@ -5,5 +5,16 @@ <menuitem name="Import & Synchronize" id="base.menu_import_crm" parent="base.menu_base_partner"/> + <record id="view_crm_meeting_search" model="ir.ui.view"> + <field name="name">CRM - Meetings Search</field> + <field name="model">calendar.event</field> + <field name="inherit_id" ref="calendar.view_calendar_event_search"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='user_id']" position="after"> + <field name="opportunity_id"/> + </xpath> + </field> + </record> + </data> </openerp> diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 67331b71ab73f71ea2cbffddc31f83fb3d27277a..432df5b4bee9c4b0622ab568560d6f7d6d88dff4 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -82,31 +82,11 @@ class crm_lead(format_address, osv.osv): context['empty_list_help_document_name'] = _("leads") return super(crm_lead, self).get_empty_list_help(cr, uid, help, context=context) - def _get_default_team_id(self, cr, uid, user_id=False, context=None): - """ Gives default team by checking if present in the context """ - team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False - return team_id - def _get_default_stage_id(self, cr, uid, context=None): """ Gives default stage_id """ - team_id = self._get_default_team_id(cr, uid, context=context) + team_id = self.pool['crm.team']._get_default_team_id(cr, uid, context=context) return self.stage_find(cr, uid, [], team_id, [('fold', '=', False)], context=context) - def _resolve_team_id_from_context(self, cr, uid, context=None): - """ Returns ID of team based on the value of 'team_id' - context key, or None if it cannot be resolved to a single - Sales Team. - """ - if context is None: - context = {} - if type(context.get('default_team_id')) in (int, long): - return context.get('default_team_id') - if isinstance(context.get('default_team_id'), basestring): - team_ids = self.pool.get('crm.team').name_search(cr, uid, name=context['default_team_id'], context=context) - if len(team_ids) == 1: - return int(team_ids[0][0]) - return None - def _resolve_type_from_context(self, cr, uid, context=None): """ Returns the type (lead or opportunity) from the type context key. Returns None if it cannot be resolved. @@ -127,7 +107,7 @@ class crm_lead(format_address, osv.osv): # - OR ('case_default', '=', True), ('fold', '=', False): add default columns that are not folded # - OR ('team_ids', '=', team_id), ('fold', '=', False) if team_id: add team columns that are not folded search_domain = [] - team_id = self._resolve_team_id_from_context(cr, uid, context=context) + team_id = self.pool['crm.team']._resolve_team_id_from_context(cr, uid, context=context) if team_id: search_domain += ['|', ('team_ids', '=', team_id)] search_domain += [('id', 'in', ids)] @@ -282,7 +262,7 @@ class crm_lead(format_address, osv.osv): 'type': 'lead', 'user_id': lambda s, cr, uid, c: uid, 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c), - 'team_id': lambda s, cr, uid, c: s._get_default_team_id(cr, uid, context=c), + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.lead', context=c), 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[0][0], 'color': 0, @@ -329,7 +309,7 @@ class crm_lead(format_address, osv.osv): def on_change_user(self, cr, uid, ids, user_id, context=None): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ - team_id = self._get_default_team_id(cr, uid, context=context) + team_id = self.pool['crm.team']._get_default_team_id(cr, uid, user_id=user_id, context=context) if user_id and not team_id and self.pool['res.users'].has_group(cr, uid, 'base.group_multi_salesteams'): team_ids = self.pool.get('crm.team').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context) if team_ids: @@ -910,6 +890,7 @@ class crm_lead(format_address, osv.osv): if lead.partner_id: partner_ids.append(lead.partner_id.id) res['context'] = { + 'search_default_opportunity_id': lead.type == 'opportunity' and lead.id or False, 'default_opportunity_id': lead.type == 'opportunity' and lead.id or False, 'default_partner_id': lead.partner_id and lead.partner_id.id or False, 'default_partner_ids': partner_ids, diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index d4e3cc2a05132b927378bedaa17e70cf706305ba..42b20752a913fe3611b259144ad30d5a657304f0 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -410,8 +410,8 @@ <field name="arch" type="xml"> <form string="Opportunities"> <header> - <button name="case_mark_won" string="Mark Won" type="object" class="oe_highlight"/> - <button name="case_mark_lost" string="Mark Lost" type="object" class="oe_highlight"/> + <button name="case_mark_won" string="Mark Won" attrs="{'invisible': [('probability', '=', 100)]}" type="object" class="oe_highlight"/> + <button name="case_mark_lost" string="Mark Lost" attrs="{'invisible': [('probability', '=', 100)]}" type="object" class="oe_highlight"/> <field name="stage_id" widget="statusbar" clickable="True" options="{'fold_field': 'fold'}" domain="['&', ('team_ids', '=', team_id), '|', ('type', '=', type), ('type', '=', 'both')]"/> diff --git a/addons/crm/crm_phonecall.py b/addons/crm/crm_phonecall.py index f7e964aee12a37a6d137ddf79a3dfc0c2df35cf6..b7885bf190b993b8e3dfb65fd3a4568eed9beaeb 100644 --- a/addons/crm/crm_phonecall.py +++ b/addons/crm/crm_phonecall.py @@ -77,6 +77,7 @@ class crm_phonecall(osv.osv): 'priority': '1', 'state': _get_default_state, 'user_id': lambda self, cr, uid, ctx: uid, + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), 'active': 1 } diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv index 5030138ae9a689a136312b6b12c59da87127a293..6330d405714571d80b2ff7a94edff2f31c77c206 100644 --- a/addons/crm/security/ir.model.access.csv +++ b/addons/crm/security/ir.model.access.csv @@ -7,6 +7,8 @@ access_crm_stage,crm.stage,model_crm_stage,,1,0,0,0 access_crm_stage_manager,crm.stage,model_crm_stage,base.group_sale_manager,1,1,1,1 access_crm_phonecall_report_user,crm.phonecall.report.user,model_crm_phonecall_report,base.group_sale_salesman,1,0,0,0 access_crm_phonecall_report_manager,crm.phonecall.report,model_crm_phonecall_report,base.group_sale_manager,1,1,1,1 +access_crm_opportunity_report_user,crm.opportunity.report,model_crm_opportunity_report,base.group_sale_salesman,1,0,0,0 +access_crm_opportunity_report_manager,crm.opportunity.report,model_crm_opportunity_report,base.group_sale_manager,1,1,1,1 access_res_partner_manager,res.partner.crm.manager,base.model_res_partner,base.group_sale_manager,1,0,0,0 access_res_partner_category_manager,res.partner.category.crm.manager,base.model_res_partner_category,base.group_sale_manager,1,0,0,0 access_res_partner,res.partner.crm.user,base.model_res_partner,base.group_sale_salesman,1,1,1,0 diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py index 286d97d996f0a7eff65ceb551b6f73ed3999d09e..d417d8a1cac910ec0023c38812cb77916990b52d 100644 --- a/addons/crm_claim/crm_claim.py +++ b/addons/crm_claim/crm_claim.py @@ -59,13 +59,9 @@ class crm_claim(osv.osv): _order = "priority,date desc" _inherit = ['mail.thread'] - def _get_default_team_id(self, cr, uid, context=None): - """ Gives default team by checking if present in the context """ - return self.pool.get('crm.lead')._resolve_team_id_from_context(cr, uid, context=context) or False - def _get_default_stage_id(self, cr, uid, context=None): """ Gives default stage_id """ - team_id = self._get_default_team_id(cr, uid, context=context) + team_id = self.pool['crm.team']._get_default_team_id(cr, uid, context=context) return self.stage_find(cr, uid, [], team_id, [('sequence', '=', '1')], context=context) _columns = { @@ -103,7 +99,7 @@ class crm_claim(osv.osv): _defaults = { 'user_id': lambda s, cr, uid, c: uid, - 'team_id': lambda s, cr, uid, c: s._get_default_team_id(cr, uid, c), + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), 'date': fields.datetime.now, 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.case', context=c), 'priority': '1', diff --git a/addons/crm_claim/test/process/claim.yml b/addons/crm_claim/test/process/claim.yml index f6c6f6d315c28cb2356c8d93d6e409227bf491a0..1a68de8ea04b544c8b475038f3326640b1c45ebb 100644 --- a/addons/crm_claim/test/process/claim.yml +++ b/addons/crm_claim/test/process/claim.yml @@ -15,7 +15,7 @@ claim_ids = self.search(cr, uid, [('email_from','=', 'Mr. John Right <info@customer.com>')]) assert claim_ids and len(claim_ids), "Claim is not created after getting request" claim = self.browse(cr, uid, claim_ids[0], context=context) - assert claim.name == tools.ustr("demande derèglement de votre produit."), "Subject does not match" + assert claim.name == tools.ustr("demande de règlement de votre produit."), "Subject does not match" - I open customer claim. - diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py index c622d45da3a529bdcbff4d2826af2c4629e30265..8d7c85bf7a7c0e7be9098c319427d7b9585b3257 100644 --- a/addons/crm_helpdesk/crm_helpdesk.py +++ b/addons/crm_helpdesk/crm_helpdesk.py @@ -81,6 +81,7 @@ class crm_helpdesk(osv.osv): 'user_id': lambda s, cr, uid, c: uid, 'state': lambda *a: 'draft', 'date': fields.datetime.now, + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c), 'priority': '1', } diff --git a/addons/crm_partner_assign/crm_lead.py b/addons/crm_partner_assign/crm_lead.py index c37cfdfd72f1a3d769380dd5030e89f220bacb62..ab7faab28ea9d3dc7e8c3aeba724ca52bac7f4e8 100644 --- a/addons/crm_partner_assign/crm_lead.py +++ b/addons/crm_partner_assign/crm_lead.py @@ -21,6 +21,7 @@ from openerp.osv import osv from openerp.tools.translate import _ +from openerp.tools.safe_eval import safe_eval as eval from openerp.exceptions import UserError diff --git a/addons/delivery/__openerp__.py b/addons/delivery/__openerp__.py index dc5878424a53c45c0bd1da521c929f6743f9fc4c..aa1906e32f5854328c622cb6748e8bbb427c0244 100644 --- a/addons/delivery/__openerp__.py +++ b/addons/delivery/__openerp__.py @@ -43,7 +43,8 @@ invoices from picking, OpenERP is able to add and compute the shipping line. 'demo': ['delivery_demo.xml'], 'test': [ '../account/test/account_minimal_test.xml', - 'test/delivery_cost.yml' + 'test/delivery_cost.yml', + 'test/stock_move_values_with_invoice_before_delivery.yml', ], 'installable': True, 'auto_install': False, diff --git a/addons/delivery/delivery.py b/addons/delivery/delivery.py index 81f88ca36dd5c294bc51560ec2833293d13fbab8..09d4e13b5fc94bc882a588b64b00d6e8f483fd98 100644 --- a/addons/delivery/delivery.py +++ b/addons/delivery/delivery.py @@ -24,6 +24,7 @@ import time from openerp.osv import fields,osv from openerp.tools.translate import _ import openerp.addons.decimal_precision as dp +from openerp.tools.safe_eval import safe_eval as eval from openerp.exceptions import UserError _logger = logging.getLogger(__name__) @@ -215,6 +216,8 @@ class delivery_grid(osv.osv): total_delivery = 0.0 product_uom_obj = self.pool.get('product.uom') for line in order.order_line: + if line.state == 'cancel': + continue if line.is_delivery: total_delivery += line.price_subtotal + self.pool['sale.order']._amount_line_tax(cr, uid, line, context=context) if not line.product_id or line.is_delivery: diff --git a/addons/delivery/sale.py b/addons/delivery/sale.py index 6de57a21fffab1eca924744fd17025957927722c..f4855769d5d37a5caa5701edb284fe831d93c750 100644 --- a/addons/delivery/sale.py +++ b/addons/delivery/sale.py @@ -66,6 +66,7 @@ class sale_order(osv.Model): carrier_obj = self.pool.get('delivery.carrier') acc_fp_obj = self.pool.get('account.fiscal.position') self._delivery_unset(cr, uid, ids, context=context) + currency_obj = self.pool.get('res.currency') for order in self.browse(cr, uid, ids, context=context): grid_id = carrier_obj.grid_get(cr, uid, [order.carrier_id.id], order.partner_shipping_id.id) if not grid_id: @@ -79,6 +80,10 @@ class sale_order(osv.Model): taxes = grid.carrier_id.product_id.taxes_id fpos = order.fiscal_position_id or False taxes_ids = acc_fp_obj.map_tax(cr, uid, fpos, taxes) + price_unit = grid_obj.get_price(cr, uid, grid.id, order, time.strftime('%Y-%m-%d'), context) + if order.company_id.currency_id.id != order.pricelist_id.currency_id.id: + price_unit = currency_obj.compute(cr, uid, order.company_id.currency_id.id, order.pricelist_id.currency_id.id, + price_unit, context=dict(context or {}, date=order.date_order)) #create the sale order line line_obj.create(cr, uid, { 'order_id': order.id, @@ -86,7 +91,7 @@ class sale_order(osv.Model): 'product_uom_qty': 1, 'product_uom': grid.carrier_id.product_id.uom_id.id, 'product_id': grid.carrier_id.product_id.id, - 'price_unit': grid_obj.get_price(cr, uid, grid.id, order, time.strftime('%Y-%m-%d'), context), + 'price_unit': price_unit, 'tax_id': [(6, 0, taxes_ids)], 'is_delivery': True }, context=context) diff --git a/addons/delivery/test/stock_move_values_with_invoice_before_delivery.yml b/addons/delivery/test/stock_move_values_with_invoice_before_delivery.yml new file mode 100644 index 0000000000000000000000000000000000000000..61fa086c5c28338755923501cf39c7c2e4f744f7 --- /dev/null +++ b/addons/delivery/test/stock_move_values_with_invoice_before_delivery.yml @@ -0,0 +1,79 @@ +- + test that the store fields of stock moves are computed with invoice before delivery flow +- + set a weight on ipod 16GB +- + !python {model: product.product, id: product.product_product_11}: | + self.write({'weight': 0.25, 'weight_net': 0.2}) +- + create a SO with invoice before delivery +- + !record {model: sale.order, id: sale_prepaid}: + partner_id: base.res_partner_18 + partner_invoice_id: base.res_partner_18 + partner_shipping_id: base.res_partner_18 + pricelist_id: product.list0 + order_policy: 'prepaid' + order_line: + - name: 'Ipod' + product_id: product.product_product_11 + product_uom_qty: 2 + product_uos_qty: 2 + product_uom: product.product_uom_unit + price_unit: 750.00 + carrier_id: normal_delivery_carrier +- + I add delivery cost in Sale order. +- + !python {model: sale.order, id: sale_prepaid}: | + self.delivery_set() +- + I confirm the SO. +- + !workflow {model: sale.order, action: order_confirm, ref: sale_prepaid} +- + I check that the invoice was created +- + !python {model: sale.order, id: sale_prepaid}: | + assert len(self.invoice_ids) == 1, "Invoice not created." +- + I confirm the invoice +- + !python {model: sale.order, id: sale_prepaid}: | + invoice = self.invoice_ids[0] + invoice.signal_workflow('invoice_open') +- + I pay the invoice. +- + !python {model: sale.order, id: sale_prepaid}: | + invoice = self.invoice_ids[0] + invoice.signal_workflow('invoice_open') + journal = self.env['account.journal'].search([('type', '=', 'cash'), + ('company_id', '=', self.company_id.id)], + limit=1) + invoice.pay_and_reconcile(journal, invoice.amount_total) +- + Check the SO after paying the invoice +- + !python {model: sale.order, id: sale_prepaid}: | + assert self.invoice_count != 0, 'order not invoiced' + assert self.invoiced, 'order is not paid' + assert len(self.picking_ids) == 1, 'pickings not generated' +- + check the stock moves +- + !python {model: sale.order, id: sale_prepaid}: | + move = self.picking_ids.move_lines + assert move.product_qty == 2, 'wrong product_qty' + assert move.weight == 0.5, 'wrong move weight' + assert move.weight_net == 0.4, 'wrong move weight_net' +- + ship +- + !python {model: sale.order, id: sale_prepaid}: | + picking = self.picking_ids.action_done() +- + Check the SO after shipping +- + !python {model: sale.order, id: sale_prepaid}: | + assert self.state == 'done' diff --git a/addons/document/document.py b/addons/document/document.py index 0d08d54e7fb011dba4b0e09c14dbf60d52e737de..de9f03c773c941850a1cee19610dc445c2a428a7 100644 --- a/addons/document/document.py +++ b/addons/document/document.py @@ -115,8 +115,11 @@ class document_file(osv.osv): ids.extend(parents[parent_id]) # sort result according to the original sort ordering - ids = [id for id in orig_ids if id in ids] - return len(ids) if count else ids + if count: + return len(ids) + else: + set_ids = set(ids) + return [id for id in orig_ids if id in set_ids] def copy(self, cr, uid, id, default=None, context=None): if not default: diff --git a/addons/event/event.py b/addons/event/event.py index f9915ff38fce946f19da68245a3c42b308fed6ce..2293d1923667ad14d44a53ecdbfb8b684be92f04 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -250,7 +250,7 @@ class event_event(models.Model): return res @api.one - def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: True): + def mail_attendees(self, template_id, force_send=True, filter_func=lambda self: True): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) @@ -336,6 +336,7 @@ class event_registration(models.Model): body=_('New registration confirmed: %s.') % (self.name or ''), subtype="event.mt_event_registration") self.state = 'open' + self.env['event.mail'].run() @api.one def button_reg_close(self): diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml index 27058212224831d2add03a43d99d6dbeca6f2a12..ea53ba9f9b0d1893395898815b769215e25432c5 100644 --- a/addons/event/event_view.xml +++ b/addons/event/event_view.xml @@ -235,7 +235,7 @@ <separator/> <filter string="My Events" help="My Events" domain="[('user_id','=',uid)]"/> <separator/> - <filter string="Upcoming" name="upcoming" domain="[('date_begin','>=',datetime.datetime.now().replace(hour=0, minute=0, second=0))]" help="Upcoming events from today" /> + <filter string="Upcoming" name="upcoming" domain="[('date_begin','>=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]" help="Upcoming events from today" /> <field name="type"/> <field name="user_id"/> <group expand="0" string="Group By"> diff --git a/addons/event/tests/test_mail_schedule.py b/addons/event/tests/test_mail_schedule.py index 752403ed22b920f238c3e26c00657c208631cade..c712e48919fa9f560a24b6414a6e983ee988794e 100644 --- a/addons/event/tests/test_mail_schedule.py +++ b/addons/event/tests/test_mail_schedule.py @@ -60,7 +60,7 @@ class TestMailSchedule(TestEventCommon): self.assertTrue(schedulers[0].done, 'event: reminder scheduler should be done') mails = self.env['mail.mail'].search([('subject', 'ilike', 'reminder'), ('date', '>=', datetime.datetime.strftime(now, tools.DEFAULT_SERVER_DATETIME_FORMAT))], order='date DESC', limit=3) - self.assertEqual(len(mails), 2, 'event: wrong number of reminder mail sent') + self.assertEqual(len(mails), 0, 'event: reminder to sent is not empty') # check subscription scheduler schedulers = self.EventMail.search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub')]) diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index d4743f22bac72a0b3ac827f4274d8ac811b062c9..df90f78f5d1c0bf932d6ae7a0f4b685876cfc23b 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -414,7 +414,7 @@ class google_calendar(osv.AbstractModel): user_obj = self.pool['res.users'] myPartnerID = user_obj.browse(cr, uid, uid, context).partner_id.id attendee_record = [] - alarm_record = [] + alarm_record = set() partner_record = [(4, myPartnerID)] result = {} @@ -461,7 +461,7 @@ class google_calendar(osv.AbstractModel): 'name': "%s minutes - %s" % (google_alarm['minutes'], google_alarm['method']) } alarm_id = [calendar_alarm_obj.create(cr, uid, data, context=context)] - alarm_record.append(alarm_id[0]) + alarm_record.add(alarm_id[0]) UTC = pytz.timezone('UTC') if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled @@ -489,7 +489,7 @@ class google_calendar(osv.AbstractModel): result.update({ 'attendee_ids': attendee_record, 'partner_ids': list(set(partner_record)), - 'alarm_ids': [(6, 0, alarm_record)], + 'alarm_ids': [(6, 0, list(alarm_record))], 'name': single_event_dict.get('summary', 'Event'), 'description': single_event_dict.get('description', False), @@ -976,7 +976,9 @@ class calendar_event(osv.Model): _inherit = "calendar.event" def get_fields_need_update_google(self, cr, uid, context=None): - return ['name', 'description', 'allday', 'date', 'date_end', 'stop', 'attendee_ids', 'alarm_ids', 'location', 'class', 'active'] + return ['name', 'description', 'allday', 'start', 'date_end', 'stop', + 'attendee_ids', 'alarm_ids', 'location', 'class', 'active', + 'start_date', 'start_datetime', 'stop_date', 'stop_datetime'] def write(self, cr, uid, ids, vals, context=None): if context is None: diff --git a/addons/google_drive/google_drive.py b/addons/google_drive/google_drive.py index 8bfc6fd7697624bd0517917f6afc1a7781108966..b26cc44860ec0ab92ed3ac36a71291855873058a 100644 --- a/addons/google_drive/google_drive.py +++ b/addons/google_drive/google_drive.py @@ -23,6 +23,7 @@ from openerp import SUPERUSER_ID from openerp.addons.google_account import TIMEOUT from openerp.osv import fields, osv from openerp.tools.translate import _ +from openerp.tools.safe_eval import safe_eval as eval from openerp.exceptions import UserError import werkzeug.urls diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py index 3e2aec8b6217a74b6b584536274171dd08bb5a7d..dc46c0e4cf9053bb96a16b062e33e35a689cff5a 100644 --- a/addons/hr_timesheet/hr_timesheet.py +++ b/addons/hr_timesheet/hr_timesheet.py @@ -159,12 +159,12 @@ class account_analytic_account(models.Model): @api.onchange('invoice_on_timesheets') def onchange_invoice_on_timesheets(self): - result = {} + result = {'value': {}} if not self.invoice_on_timesheets: return {'value': {'to_invoice': False}} try: to_invoice = self.env['ir.model.data'].xmlid_to_res_id('hr_timesheet_invoice.timesheet_invoice_factor1') - result['to_invoice'] = to_invoice + result['value']['to_invoice'] = to_invoice except ValueError: pass return result diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index d622d9310bfb113f80e8fac64a848d21d4209499..a706cbdbf954795af99decd483fae07cb5650414 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -314,8 +314,8 @@ class account_analytic_line(osv.osv): res['value']['to_invoice'] = st or False if acc.state == 'pending': res['warning'] = { - 'title': 'Warning', - 'message': 'The analytic account is in pending state.\nYou should not work on this account !' + 'title': _('Warning'), + 'message': _('The analytic account is in pending state.\nYou should not work on this account !') } elif acc.state == 'close' or acc.state == 'cancelled': raise osv.except_osv(_('Invalid Analytic Account!'), _('You cannot select a Analytic Account which is in Close or Cancelled state.')) diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index 64f6cf0765c3fe8c29bb5fa08aa7e7c8fd93ec17..337874cf67ade02740d9af7f4b3083af7b0bae5b 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -192,12 +192,12 @@ class Proxy(http.Controller): print 'print_receipt' + str(receipt) @http.route('/hw_proxy/is_scanner_connected', type='json', auth='none', cors='*') - def print_receipt(self, receipt): + def is_scanner_connected(self, receipt): print 'is_scanner_connected?' return False @http.route('/hw_proxy/scanner', type='json', auth='none', cors='*') - def print_receipt(self, receipt): + def scanner(self, receipt): print 'scanner' time.sleep(10) return '' diff --git a/addons/hw_scale/__openerp__.py b/addons/hw_scale/__openerp__.py index 6c83a854730ae8bf29f7e81b66f9083a4000b45e..387a7e618d96cece3d9cf39b30da5967f756eedb 100644 --- a/addons/hw_scale/__openerp__.py +++ b/addons/hw_scale/__openerp__.py @@ -37,6 +37,7 @@ such as the Mettler Toledo Ariva. """, 'author': 'OpenERP SA', 'depends': ['hw_proxy'], + 'external_dependencies': {'python': ['serial']}, 'test': [ ], 'installable': True, diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py index 531f2739f553e2f1a429d0fa89defdc879b8975b..44a031f55668e7a24d9a0fc5e8c4a960f12dc1cc 100644 --- a/addons/hw_scale/controllers/main.py +++ b/addons/hw_scale/controllers/main.py @@ -178,28 +178,34 @@ class Scale(Thread): if not self.device: time.sleep(5) -s = Scale() - -hw_proxy.drivers['scale'] = s +scale_thread = None +if serial: + scale_thread = Scale() + hw_proxy.drivers['scale'] = scale_thread class ScaleDriver(hw_proxy.Proxy): @http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*') def scale_read(self): - return {'weight':s.get_weight(), 'unit':'kg', 'info':s.get_weight_info()} + if scale_thread: + return {'weight': scale_thread.get_weight(), 'unit':'kg', 'info': scale_thread.get_weight_info()} + return None @http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*') def scale_zero(self): - s.set_zero() + if scale_thread: + scale_thread.set_zero() return True @http.route('/hw_proxy/scale_tare/', type='json', auth='none', cors='*') def scale_tare(self): - s.set_tare() + if scale_thread: + scale_thread.set_tare() return True @http.route('/hw_proxy/scale_clear_tare/', type='json', auth='none', cors='*') def scale_clear_tare(self): - s.clear_tare() + if scale_thread: + scale_thread.clear_tare() return True diff --git a/addons/hw_scanner/__openerp__.py b/addons/hw_scanner/__openerp__.py index aa74b0fbc5b43e5fad20e296a39fe1619d9e93fb..f38fa7dc42059ad936e4d683ecee4802bf0db847 100644 --- a/addons/hw_scanner/__openerp__.py +++ b/addons/hw_scanner/__openerp__.py @@ -38,6 +38,7 @@ point of sale module. """, 'author': 'OpenERP SA', 'depends': ['hw_proxy'], + 'external_dependencies': {'python': ['evdev']}, 'test': [ ], 'installable': True, diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py index 057674c1ca1021e215c9601fada93922d6a70154..6e53c37006f5629c7364d0be2f47c2369519938b 100644 --- a/addons/hw_scanner/controllers/main.py +++ b/addons/hw_scanner/controllers/main.py @@ -111,8 +111,6 @@ class Scanner(Thread): def get_device(self): try: - if not evdev: - return None devices = [ device for device in listdir(self.input_dir)] keyboards = [ device for device in devices if ('kbd' in device) and ('keyboard' not in device.lower())] scanners = [ device for device in devices if ('barcode' in device.lower()) or ('scanner' in device.lower())] @@ -135,7 +133,6 @@ class Scanner(Thread): been returned before. This is necessary to catch barcodes scanned while the POS is busy reading another barcode """ - self.lockedstart() while True: @@ -204,12 +201,12 @@ class Scanner(Thread): except Exception as e: self.set_status('error',str(e)) -s = Scanner() - -hw_proxy.drivers['scanner'] = s +scanner_thread = None +if evdev: + scanner_thread = Scanner() + hw_proxy.drivers['scanner'] = scanner_thread class ScannerDriver(hw_proxy.Proxy): @http.route('/hw_proxy/scanner', type='json', auth='none', cors='*') def scanner(self): - return s.get_barcode() - + return scanner_thread.get_barcode() if scanner_thread else None diff --git a/addons/im_chat/static/src/xml/im_chat.xml b/addons/im_chat/static/src/xml/im_chat.xml index 9adef41a24f96ec7c84daca6dcb5c7f74c365e7e..80447cc38d063e79932a81a88dbcc23dc524be46 100644 --- a/addons/im_chat/static/src/xml/im_chat.xml +++ b/addons/im_chat/static/src/xml/im_chat.xml @@ -87,7 +87,7 @@ <div class="oe_im"> <div class="oe_im_frame_header"> <i class="fa fa-search oe_im_search_icon"/> - <input class="oe_im_searchbox" t-att-placeholder="_t('Search users...')"/> + <input class="oe_im_searchbox" placeholder="Search users..."/> </div> <div class="oe_im_users"></div> <div class="oe_im_content"></div> diff --git a/addons/im_livechat/models/im_chat_session.py b/addons/im_livechat/models/im_chat_session.py index 0cae7f76c465f4827dad62f31e02e03c5729a3b0..f4c00329a1157cf0c5a34f8a5745c2b27e9eea34 100644 --- a/addons/im_livechat/models/im_chat_session.py +++ b/addons/im_livechat/models/im_chat_session.py @@ -49,8 +49,11 @@ class ImChatSession(models.Model): """ self.ensure_one() users_infos = super(ImChatSession, self).session_user_info() + # identify the operator for the 'welcome message' + for user_profile in users_infos: + user_profile['is_operator'] = bool(user_profile['id'] == self.env._context.get('im_livechat_operator_id')) if self.anonymous_name: - users_infos.append({'id': False, 'name': self.anonymous_name, 'im_status': 'online'}) + users_infos.append({'id': False, 'name': self.anonymous_name, 'im_status': 'online', 'is_operator': False}) return users_infos @api.model diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 258b4e2aeb1018b8c382fac299fd8d2b8e87970a..85b202e002f8c96b1466cde5a9feced19c6f0c2e 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -171,7 +171,7 @@ class ImLivechatChannel(models.Model): user_to_add.append((4, self.env.uid)) # create the session, and add the link with the given channel session = self.env["im_chat.session"].sudo().create({'user_ids': user_to_add, 'channel_id': channel_id, 'anonymous_name': anonymous_name}) - return session.sudo().session_info() + return session.sudo().with_context(im_livechat_operator_id=user_id).session_info() @api.model def get_channel_infos(self, channel_id): diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 88c234378a0f75eda2216138b22a9ac1de7c5898..15f3f5bae09c429872573ca2452817c847cc74f0 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -203,12 +203,13 @@ var ChatButton = Widget.extend({ if(this.session.users.length > 0){ if (self.options.defaultMessage) { setTimeout(function(){ + var operator = _.last(_.filter(self.session.users, function(user){return user.is_operator})); self.conv.message_receive({ id : 1, type: "message", message: self.options.defaultMessage, create_date: time.datetime_to_str(new Date()), - from_id: [self.session.users[0].id, self.session.users[0].name], + from_id: [operator.id, operator.name], to_id: [0, self.session.uuid] }); }, 1000); diff --git a/addons/im_odoo_support/static/src/js/im_odoo_support.js b/addons/im_odoo_support/static/src/js/im_odoo_support.js index 4945ba49ca2a4a18bf504a1135ac151d98924620..ec61c18af06ee531d1dc2c839ae5803ae1c0db5b 100644 --- a/addons/im_odoo_support/static/src/js/im_odoo_support.js +++ b/addons/im_odoo_support/static/src/js/im_odoo_support.js @@ -11,7 +11,7 @@ var Widget = require('web.Widget'); var _t = core._t; -var COOKIE_NAME = 'livechat_conversation'; +var COOKIE_NAME = 'odoo_livechat_conversation'; var SERVICE_URL = 'https://services.odoo.com/'; var OdooSupport = Widget.extend({ diff --git a/addons/l10n_ca/account_chart_en.xml b/addons/l10n_ca/account_chart_en.xml index fb7532e6ff875205a75c8d0710e9ff3a9461219d..86228db2c35e6c5c72de06cc3e4d48b0fd5cfbbf 100644 --- a/addons/l10n_ca/account_chart_en.xml +++ b/addons/l10n_ca/account_chart_en.xml @@ -259,101 +259,119 @@ <field name="name">LABOUR TAXES TO PAY</field> </record> - <record id="chart2141_en" model="account.account.template"> - <field name="code">2141</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">CANADIAN REVENU AGENCY</field> - </record> - - <record id="chart21411_en" model="account.account.template"> - <field name="code">21411</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">EMPLOYMENT INSURANCE TO PAY</field> - </record> - - <record id="chart214111_en" model="account.account.template"> - <field name="code">214111</field> + <record id="chart2141_en" model="account.account.template"> + <field name="code">2141</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">CANADA REVENUE AGENCY</field> + </record> + + <record id="chart21411_en" model="account.account.template"> + <field name="code">21411</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">EMPLOYMENT INSURANCE TO PAY</field> + </record> + + <record id="chart214111_en" model="account.account.template"> + <field name="code">214111</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">EI - Employees Contribution</field> - </record> + <field name="name">EI - Employees Contribution</field> + </record> - <record id="chart214112_en" model="account.account.template"> - <field name="code">214112</field> + <record id="chart214112_en" model="account.account.template"> + <field name="code">214112</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">EI - Employer Contribution</field> - </record> + <field name="name">EI - Employer Contribution</field> + </record> - <record id="chart21412_en" model="account.account.template"> - <field name="code">21412</field> + <record id="chart21412_en" model="account.account.template"> + <field name="code">21412</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Federal Income Tax</field> - </record> - - <record id="chart2142_en" model="account.account.template"> - <field name="code">2142</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">PROVINCIAL REVENU AGENCY</field> - </record> - - <record id="chart21421_en" model="account.account.template"> - <field name="code">21421</field> + <field name="name">Federal Income Tax</field> + </record> + + <record id="chart21413_en" model="account.account.template"> + <field name="code">21413</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">CANADA PENSION PLAN TO PAY</field> + </record> + + <record id="chart214131_en" model="account.account.template"> + <field name="code">214131</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">CPP - Employees Contribution</field> + </record> + + <record id="chart214132_en" model="account.account.template"> + <field name="code">214132</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">CPP - Employer Contribution</field> + </record> + + <record id="chart2142_en" model="account.account.template"> + <field name="code">2142</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">PROVINCIAL REVENU AGENCY</field> + </record> + + <record id="chart21421_en" model="account.account.template"> + <field name="code">21421</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Health Services Fund to pay</field> - </record> + <field name="name">Health Services Fund to pay</field> + </record> - <record id="chart21422_en" model="account.account.template"> - <field name="code">21422</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">ANNUITIES TO PAY</field> - </record> + <record id="chart21422_en" model="account.account.template"> + <field name="code">21422</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">PROVINCIAL PENSION PLAN TO PAY</field> + </record> - <record id="chart214221_en" model="account.account.template"> - <field name="code">214221</field> + <record id="chart214221_en" model="account.account.template"> + <field name="code">214221</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Annuities - Employees Contribution</field> - </record> + <field name="name">Provincial Pension Plan - Employees Contribution</field> + </record> - <record id="chart214222_en" model="account.account.template"> - <field name="code">214222</field> + <record id="chart214222_en" model="account.account.template"> + <field name="code">214222</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Annuities - Employer Contribution</field> - </record> + <field name="name">Provincial Pension Plan - Employer Contribution</field> + </record> - <record id="chart21423_en" model="account.account.template"> - <field name="code">21423</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">PARENTAL INSURANCE PLAN TO PAY</field> - </record> + <record id="chart21423_en" model="account.account.template"> + <field name="code">21423</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">PROVINCIAL PARENTAL INSURANCE PLAN TO PAY</field> + </record> - <record id="chart214231_en" model="account.account.template"> - <field name="code">214231</field> + <record id="chart214231_en" model="account.account.template"> + <field name="code">214231</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">PAP - Employee Contribution</field> - </record> + <field name="name">Parental Insurance Plan - Employee Contribution</field> + </record> - <record id="chart214232_en" model="account.account.template"> - <field name="code">214232</field> + <record id="chart214232_en" model="account.account.template"> + <field name="code">214232</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">PAP - Employer Contribution</field> - </record> + <field name="name">Parental Insurance Plan - Employer Contribution</field> + </record> - <record id="chart21424_en" model="account.account.template"> - <field name="code">21424</field> + <record id="chart21424_en" model="account.account.template"> + <field name="code">21424</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Labour Health and Safety to pay</field> - </record> + <field name="name">Labour Health and Safety to pay</field> + </record> - <record id="chart21425_en" model="account.account.template"> - <field name="code">21425</field> + <record id="chart21425_en" model="account.account.template"> + <field name="code">21425</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Labour Standards to pay</field> - </record> + <field name="name">Labour Standards to pay</field> + </record> - <record id="chart21426_en" model="account.account.template"> - <field name="code">21426</field> + <record id="chart21426_en" model="account.account.template"> + <field name="code">21426</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Provincial Income Tax</field> - </record> + <field name="name">Provincial Income Tax</field> + </record> <record id="chart215_en" model="account.account.template"> <field name="code">215</field> @@ -379,6 +397,66 @@ <field name="name">Stock Received But Not Billed</field> </record> + <record id="chart218_en" model="account.account.template"> + <field name="code">218</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">CURRENT LIABILITIES RELATED TO SALARIES</field> + </record> + + <record id="chart2181_en" model="account.account.template"> + <field name="code">2181</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Salaries to pay</field> + </record> + + <record id="chart2183_en" model="account.account.template"> + <field name="code">2183</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Bonus to pay</field> + </record> + + <record id="chart2184_en" model="account.account.template"> + <field name="code">2184</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Retroactive Payment to pay</field> + </record> + + <record id="chart2185_en" model="account.account.template"> + <field name="code">2185</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">GROUP PENSION PLAN TO PAY</field> + </record> + + <record id="chart218501_en" model="account.account.template"> + <field name="code">218501</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Group Pension Plan to pay - Employees Contribution</field> + </record> + + <record id="chart218502_en" model="account.account.template"> + <field name="code">218502</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Group Pension Plan to pay - Employer Contribution</field> + </record> + + <record id="chart2186_en" model="account.account.template"> + <field name="code">2186</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">EMPLOYEE BENEFITS</field> + </record> + + <record id="chart218601_en" model="account.account.template"> + <field name="code">218601</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Employee Benefits Provision - Employees Contribution</field> + </record> + + <record id="chart218602_en" model="account.account.template"> + <field name="code">218602</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Employee Benefits Provision - Employer Contribution</field> + </record> + <record id="chart25_en" model="account.account.template"> <field name="code">25</field> <field name="user_type" ref="account.data_account_type_view"/> @@ -387,8 +465,6 @@ <record id="chart251_en" model="account.account.template"> <field name="code">251</field> - <field name="parent_id" ref="chart25_en"/> - <field name="type">view</field> <field name="user_type" ref="account.data_account_type_view"/> <field name="name">NON-CURRENT FINANCIAL DEBTS</field> </record> @@ -399,12 +475,54 @@ <field name="name">PROVISIONS FOR PENSIONS AND OTHER POST-EMPLOYMENT ADVANTAGES</field> </record> - <record id="chart254_en" model="account.account.template"> - <field name="code">254</field> + <record id="chart2521_en" model="account.account.template"> + <field name="code">2521</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Provision for pension plans</field> + </record> + + <record id="chart253_en" model="account.account.template"> + <field name="code">253</field> <field name="user_type" ref="account.data_account_type_view"/> <field name="name">DEFERRED TAXES</field> </record> + <record id="chart254_en" model="account.account.template"> + <field name="code">254</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">NON-CURRENT LIABILITIES RELATED TO SALARIES</field> + </record> + + <record id="chart2541_en" model="account.account.template"> + <field name="code">2541</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Leaves Accruded</field> + </record> + + <record id="chart254101_en" model="account.account.template"> + <field name="code">254101</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Vacations Accruded</field> + </record> + + <record id="chart254102_en" model="account.account.template"> + <field name="code">254102</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Compensatory Days Accruded</field> + </record> + + <record id="chart254103_en" model="account.account.template"> + <field name="code">254103</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Sick Leaves Accruded</field> + </record> + + <record id="chart2542_en" model="account.account.template"> + <field name="code">2542</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Bonus Accruded</field> + </record> + <record id="chart259_en" model="account.account.template"> <field name="code">259</field> <field name="user_type" ref="account.data_account_type_view"/> @@ -561,71 +679,127 @@ <field name="name">International Purchases</field> </record> - <record id="chart512_en" model="account.account.template"> - <field name="code">512</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">LABOUR EXPENSES</field> - </record> - - <record id="chart51201_en" model="account.account.template"> - <field name="code">51201</field> + <record id="chart512_en" model="account.account.template"> + <field name="code">512</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">LABOUR EXPENSES</field> + </record> + + <record id="chart5121_en" model="account.account.template"> + <field name="code">5121</field> + + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Salaries</field> + </record> + + <record id="chart512101_en" model="account.account.template"> + <field name="code">512101</field> + + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Regular Salaries</field> + </record> + + <record id="chart512102_en" model="account.account.template"> + <field name="code">512102</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Bonus</field> + </record> + + <record id="chart512103_en" model="account.account.template"> + <field name="code">512103</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Retroactive Pay</field> + </record> + + <record id="chart5122_en" model="account.account.template"> + <field name="code">5122</field> + + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Leaves Accruded</field> + </record> + + <record id="chart512201_en" model="account.account.template"> + <field name="code">512201</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Vacations Accruded</field> + </record> + + <record id="chart512202_en" model="account.account.template"> + <field name="code">512202</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + + <field name="name">Compensatory Days Accruded</field> + </record> + + <record id="chart512203_en" model="account.account.template"> + <field name="code">512203</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Sick Leaves Accruded</field> + </record> + + <record id="chart5123_en" model="account.account.template"> + <field name="code">5123</field> + + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Employer Contributions</field> + </record> + + <record id="chart512301_en" model="account.account.template"> + <field name="code">512301</field> + + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Canada Pension Plan</field> + </record> + + <record id="chart512302_en" model="account.account.template"> + <field name="code">512302</field> + + <field name="user_type" ref="account.data_account_type_expe + <field name="name">Employment Insurance</field> + </record> + + <record id="chart512303_en" model="account.account.template"> + <field name="code">512303</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Salaries, wages and commissions</field> - </record> + <field name="name">Group Pension Plan</field> + </record> - <record id="chart51202_en" model="account.account.template"> - <field name="code">51202</field> + <record id="chart512304_en" model="account.account.template"> + <field name="code">512304</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Holidays</field> - </record> + <field name="name">Employee benefits expense</field> + </record> - <record id="chart51203_en" model="account.account.template"> - <field name="code">51203</field> + <record id="chart512310_en" model="account.account.template"> + <field name="code">512310</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Employment Insurance</field> - </record> + <field name="name">Provincial Pension Plan</field> + </record> - <record id="chart51204_en" model="account.account.template"> - <field name="code">51204</field> + <record id="chart512311_en" model="account.account.template"> + <field name="code">512311</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Health Services Fund</field> - </record> + <field name="name">Provincial Parental Insurance Plan</field> + </record> - <record id="chart51205_en" model="account.account.template"> - <field name="code">51205</field> + <record id="chart512312_en" model="account.account.template"> + <field name="code">512312</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Annuities</field> - </record> - - <record id="chart51206_en" model="account.account.template"> - <field name="code">51206</field> - <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Parental Insurance</field> - </record> + <field name="name">Labour Health and Safety</field> + </record> - <record id="chart51207_en" model="account.account.template"> - <field name="code">51207</field> + <record id="chart512313_en" model="account.account.template"> + <field name="code">512313</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Labour Health and Safety</field> - </record> + <field name="name">Labour Standards</field> + </record> - <record id="chart51208_en" model="account.account.template"> - <field name="code">51208</field> + <record id="chart512314_en" model="account.account.template"> + <field name="code">512314</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Labour Standards</field> - </record> + <field name="name">Health Service Fund</field> + </record> - <record id="chart51209_en" model="account.account.template"> - <field name="code">51209</field> - <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Federal Income Tax</field> - </record> - - <record id="chart51210_en" model="account.account.template"> - <field name="code">51210</field> - <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Provincial Income Tax</field> - </record> <record id="chart513_en" model="account.account.template"> <field name="code">513</field> <field name="user_type" ref="account.data_account_type_view"/> diff --git a/addons/l10n_ca/account_chart_fr.xml b/addons/l10n_ca/account_chart_fr.xml index 00674ac4e12b8bb4d7d6804d0b7fcc5047c11675..b24de09d845cbd14ac5e9b33d8244af55eaccbb2 100644 --- a/addons/l10n_ca/account_chart_fr.xml +++ b/addons/l10n_ca/account_chart_fr.xml @@ -253,107 +253,127 @@ <field name="name">TVH à payer - 15%</field> </record> - <record id="chart214_fr" model="account.account.template"> - <field name="code">214</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">IMPÔTS LIÉS AUX SALAIRES À PAYER</field> - </record> - - <record id="chart2141_fr" model="account.account.template"> - <field name="code">2141</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">AGENCE DU REVENU DU CANADA</field> - </record> - - <record id="chart21411_fr" model="account.account.template"> - <field name="code">21411</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">ASSURANCE EMPLOI À PAYER</field> - </record> - - <record id="chart214111_fr" model="account.account.template"> - <field name="code">214111</field> + <record id="chart214_fr" model="account.account.template"> + <field name="code">214</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">DAS ET CONTRIBUTIONS DE L'EMPLOYEUR À PAYER</field> + </record> + + <record id="chart2141_fr" model="account.account.template"> + <field name="code">2141</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">AGENCE DU REVENU DU CANADA</field> + </record> + + <record id="chart21411_fr" model="account.account.template"> + <field name="code">21411</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">ASSURANCE EMPLOI À PAYER</field> + </record> + + <record id="chart214111_fr" model="account.account.template"> + <field name="code">214111</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">AE - Contribution des employés</field> - </record> + <field name="name">AE - Contribution des employés</field> + </record> - <record id="chart214112_fr" model="account.account.template"> - <field name="code">214112</field> + <record id="chart214112_fr" model="account.account.template"> + <field name="code">214112</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">AE - Contribution de l'employeur</field> - </record> + <field name="name">AE - Contribution de l'employeur</field> + </record> - <record id="chart21412_fr" model="account.account.template"> - <field name="code">21412</field> + <record id="chart21412_fr" model="account.account.template"> + <field name="code">21412</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Impôt fédéral sur les revenus</field> - </record> + <field name="name">Impôt fédéral sur les revenus</field> + </record> - <record id="chart2142_fr" model="account.account.template"> - <field name="code">2142</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">AGENCE DU REVENU PROVINCIAL</field> - </record> + <record id="chart21413_fr" model="account.account.template"> + <field name="code">21413</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">RÉGIME DE PENSIONS DU CANADA À PAYER</field> + </record> + + <record id="chart214131_fr" model="account.account.template"> + <field name="code">214131</field> + + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">RPC - Contribution des employés</field> + </record> + + <record id="chart214132_fr" model="account.account.template"> + <field name="code">214132</field> - <record id="chart21421_fr" model="account.account.template"> - <field name="code">21421</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">RPC - Contribution de l'employeur</field> + </record> + + <record id="chart2142_fr" model="account.account.template"> + <field name="code">2142</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">AGENCE PROVINCIALE</field> + </record> + + <record id="chart21421_fr" model="account.account.template"> + <field name="code">21421</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Fond des Services de Santé à payer</field> - </record> + <field name="name">Fond des Services de Santé à payer</field> + </record> - <record id="chart21422_fr" model="account.account.template"> - <field name="code">21422</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">RENTES À PAYER</field> - </record> + <record id="chart21422_fr" model="account.account.template"> + <field name="code">21422</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">RÉGIME DE PENSION PROVINCIAL À PAYER</field> + </record> - <record id="chart214221_fr" model="account.account.template"> - <field name="code">214221</field> + <record id="chart214221_fr" model="account.account.template"> + <field name="code">214221</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Rentes - Contribution des employés</field> - </record> + <field name="name">Régime de pension provincial - Contribution des employés</field> + </record> - <record id="chart214222_fr" model="account.account.template"> - <field name="code">214222</field> + <record id="chart214222_fr" model="account.account.template"> + <field name="code">214222</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Rentes - Contribution de l'employeur</field> - </record> + <field name="name">Régime de pension provincial - Contribution de l'employeur</field> + </record> - <record id="chart21423_fr" model="account.account.template"> - <field name="code">21423</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">ASSURANCE PARENTALE À PAYER</field> - </record> - - <record id="chart214231_fr" model="account.account.template"> - <field name="code">214231</field> + <record id="chart21423_fr" model="account.account.template"> + <field name="code">21423</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">RÉGIME PROVINCIAL D'ASSURANCE PARENTALE À PAYER</field> + </record> + + <record id="chart214231_fr" model="account.account.template"> + <field name="code">214231</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">AP - Contribution des employés</field> - </record> - - <record id="chart214232_fr" model="account.account.template"> - <field name="code">214232</field> + <field name="name">Assurance parentale - Contribution des employés</field> + </record> + + <record id="chart214232_fr" model="account.account.template"> + <field name="code">214232</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">AP - Contribution de l'employeur</field> - </record> + <field name="name">Assurance parentale - Contribution de l'employeur</field> + </record> - <record id="chart21424_fr" model="account.account.template"> - <field name="code">21424</field> + <record id="chart21424_fr" model="account.account.template"> + <field name="code">21424</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Santé et Sécurité au Travail à payer</field> - </record> + <field name="name">Santé et Sécurité au Travail à payer</field> + </record> - <record id="chart21425_fr" model="account.account.template"> - <field name="code">21425</field> + <record id="chart21425_fr" model="account.account.template"> + <field name="code">21425</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Normes du Travail à payer</field> - </record> + <field name="name">Normes du Travail à payer</field> + </record> - <record id="chart21426_fr" model="account.account.template"> - <field name="code">21426</field> + <record id="chart21426_fr" model="account.account.template"> + <field name="code">21426</field> <field name="user_type" ref="account.data_account_type_current_liabilities"/> - <field name="name">Impôt provincial sur les revenus</field> - </record> + <field name="name">Impôt provincial sur les revenus</field> + </record> <record id="chart215_fr" model="account.account.template"> <field name="code">215</field> @@ -379,6 +399,67 @@ <field name="name">Stock reçu non facturé</field> </record> + <record id="chart218_fr" model="account.account.template"> + <field name="code">218</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">PASSIFS COURANTS LIÉS AUX SALAIRES</field> + </record> + + <record id="chart2181_fr" model="account.account.template"> + <field name="code">2181</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Salaires à payer</field> + </record> + + <record id="chart2183_fr" model="account.account.template"> + <field name="code">2183</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Bonis à payer</field> + </record> + + <record id="chart2184_fr" model="account.account.template"> + <field name="code">2184</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Paie Rétroactive à payer</field> + </record> + + <!-- Montants à verser dans un RVER, RPA ou autre régime collectif --> + <record id="chart2185_fr" model="account.account.template"> + <field name="code">2185</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">RÉGIMES DE PENSION COLLECTIFS À PAYER</field> + </record> + + <record id="chart218501_fr" model="account.account.template"> + <field name="code">218501</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Régimes de pension collectifs à payer - Contribution des employés</field> + </record> + + <record id="chart218502_fr" model="account.account.template"> + <field name="code">218502</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Régimes de pension collectifs à payer - Contribution de l'employeur</field> + </record> + + <record id="chart2186_fr" model="account.account.template"> + <field name="code">2186</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">PROVISION POUR AVANTAGES SOCIAUX</field> + </record> + + <record id="chart218601_fr" model="account.account.template"> + <field name="code">218601</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Provision pour avantages sociaux - Contribution de l'employé</field> + </record> + + <record id="chart218602_fr" model="account.account.template"> + <field name="code">218602</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Provision pour avantages sociaux - Contribution de l'employeur</field> + </record> + <record id="chart25_fr" model="account.account.template"> <field name="code">25</field> <field name="user_type" ref="account.data_account_type_view"/> @@ -397,17 +478,53 @@ <field name="name">PROVISIONS POUR RETRAITES ET AUTRES AVANTAGES POSTÉRIEURS À L'EMPLOI</field> </record> - <record id="chart253_fr" model="account.account.template"> - <field name="code">253</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">IMPÔTS DIFFÉRÉS</field> - </record> - - <record id="chart254_fr" model="account.account.template"> - <field name="code">254</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">AUTRES PASSIFS NON-COURANTS</field> - </record> + <record id="chart253_fr" model="account.account.template"> + <field name="code">253</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">IMPÔTS DIFFÉRÉS</field> + </record> + + <record id="chart254_fr" model="account.account.template"> + <field name="code">254</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">PASSIFS NON-COURANTS LIÉS AUX SALAIRES</field> + </record> + + <record id="chart2541_fr" model="account.account.template"> + <field name="code">2541</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Congés Accumulés</field> + </record> + + <record id="chart254101_fr" model="account.account.template"> + <field name="code">254101</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Vacances Accumulées</field> + </record> + + <record id="chart254102_fr" model="account.account.template"> + <field name="code">254102</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Jours Compensatoires Accumulés</field> + </record> + + <record id="chart254103_fr" model="account.account.template"> + <field name="code">254103</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Congés de Maladie Accumulés</field> + </record> + + <record id="chart2542_fr" model="account.account.template"> + <field name="code">2542</field> + <field name="user_type" ref="account.data_account_type_current_liabilities"/> + <field name="name">Bonis Accumulés</field> + </record> + + <record id="chart259_fr" model="account.account.template"> + <field name="code">259</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">AUTRES PASSIFS NON-COURANTS</field> + </record> <!-- CAPITAUX PROPRES --> @@ -565,71 +682,125 @@ <field name="name">Achats à l'étranger</field> </record> - <record id="chart512_fr" model="account.account.template"> - <field name="code">512</field> - <field name="user_type" ref="account.data_account_type_view"/> - <field name="name">SALAIRES ET CHARGES SOCIALES</field> - </record> - - <record id="chart51201_fr" model="account.account.template"> - <field name="code">51201</field> - <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Salaires</field> - </record> - - <record id="chart51202_fr" model="account.account.template"> - <field name="code">51202</field> - <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Vacances</field> - </record> - - <record id="chart51203_fr" model="account.account.template"> - <field name="code">51203</field> - <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Assurance Emploi</field> - </record> - - <record id="chart51204_fr" model="account.account.template"> - <field name="code">51204</field> + <record id="chart512_fr" model="account.account.template"> + <field name="code">512</field> + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">SALAIRES ET CHARGES SOCIALES</field> + </record> + + <record id="chart5121_fr" model="account.account.template"> + <field name="code">5121</field> + + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Salaires</field> + </record> + + <record id="chart512101_fr" model="account.account.template"> + <field name="code">512101</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Salaires Réguliers</field> + </record> + + <record id="chart512102_fr" model="account.account.template"> + <field name="code">512102</field> + + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Bonis</field> + </record> + + <record id="chart512103_fr" model="account.account.template"> + <field name="code">512103</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Paies Rétroactives</field> + </record> + + <record id="chart5122_fr" model="account.account.template"> + <field name="code">5122</field> + + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Congés Accumulées</field> + </record> + + <record id="chart512201_fr" model="account.account.template"> + <field name="code">512201</field> +<field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Vacances Accumulées</field> + </record> + + <record id="chart512202_fr" model="account.account.template"> + <field name="code">512202</field> + + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Jours Compensatoires Accumulées</field> + </record> + + <record id="chart512203_fr" model="account.account.template"> + <field name="code">512203</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Congés de Maladie Accumulés</field> + </record> + + <record id="chart5123_fr" model="account.account.template"> + <field name="code">5123</field> + + <field name="user_type" ref="account.data_account_type_view"/> + <field name="name">Contributions de l'Employeur</field> + </record> + + <record id="chart512301_fr" model="account.account.template"> + <field name="code">512301</field> + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Régime de Pensions du Canada</field> + </record> + + <record id="chart512302_fr" model="account.account.template"> + <field name="code">512302</field> + + <field name="user_type" ref="account.data_account_type_expenses"/> + <field name="name">Assurance Emploi</field> + </record> + + <record id="chart512303_fr" model="account.account.template"> + <field name="code">512303</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Fonds des services de santé</field> - </record> + <field name="name">Régimes de pension collectifs</field> + </record> - <record id="chart51205_fr" model="account.account.template"> - <field name="code">51205</field> + <record id="chart512304_fr" model="account.account.template"> + <field name="code">512304</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Rentes</field> - </record> + <field name="name">Dépense d'avantages sociaux</field> + </record> - <record id="chart51206_fr" model="account.account.template"> - <field name="code">51206</field> + <record id="chart512310_fr" model="account.account.template"> + <field name="code">512310</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Assurance parentale</field> - </record> + <field name="name">Régime de pension provincial</field> + </record> - <record id="chart51207_fr" model="account.account.template"> - <field name="code">51207</field> + <record id="chart512311_fr" model="account.account.template"> + <field name="code">512311</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Santé et sécurité au travail</field> - </record> + <field name="name">Régime d'assurance parental provincial</field> + </record> - <record id="chart51208_fr" model="account.account.template"> - <field name="code">51208</field> + <record id="chart512312_fr" model="account.account.template"> + <field name="code">512312</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Normes du travail</field> - </record> + <field name="name">Santé et sécurité au travail</field> + </record> - <record id="chart51209_fr" model="account.account.template"> - <field name="code">51209</field> + <record id="chart512313_fr" model="account.account.template"> + <field name="code">512313</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Impôt fédéral</field> - </record> + <field name="name">Normes du travail</field> + </record> - <record id="chart51210_fr" model="account.account.template"> - <field name="code">51210</field> + <record id="chart512314_fr" model="account.account.template"> + <field name="code">512314</field> <field name="user_type" ref="account.data_account_type_expenses"/> - <field name="name">Impôt provincial</field> - </record> + <field name="name">Fonds des services de santé</field> + </record> <record id="chart513_fr" model="account.account.template"> <field name="code">513</field> diff --git a/addons/l10n_ca/account_tax_en.xml b/addons/l10n_ca/account_tax_en.xml index 54073d0f5459e76859a7d35dcf68dfdd55aff9e1..10e7bc794834cab460bf46b1aa22086fdbc18911 100644 --- a/addons/l10n_ca/account_tax_en.xml +++ b/addons/l10n_ca/account_tax_en.xml @@ -14,6 +14,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend">1</field> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstpst_sale_bc_gst_en" model="account.tax.template"> @@ -28,6 +30,8 @@ <field name="account_collected_id" ref="chart2131_en"/> <field name="account_paid_id" ref="chart2131_en"/> <field name="parent_id" ref="gstpst_bc_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="pst_bc_sale_en" model="account.tax.template"> @@ -41,6 +45,8 @@ <field name="account_collected_id" ref="chart2132_en"/> <field name="account_paid_id" ref="chart2132_en"/> <field name="parent_id" ref="gstpst_bc_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- Manitoba PST --> @@ -53,6 +59,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstpst_sale_mb_gst_en" model="account.tax.template"> @@ -67,6 +75,8 @@ <field name="account_collected_id" ref="chart2131_en"/> <field name="account_paid_id" ref="chart2131_en"/> <field name="parent_id" ref="gstpst_mb_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="pst_mb_sale_en" model="account.tax.template"> @@ -80,6 +90,8 @@ <field name="account_collected_id" ref="chart2132_en"/> <field name="account_paid_id" ref="chart2132_en"/> <field name="parent_id" ref="gstpst_mb_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- Quebec PST --> @@ -92,6 +104,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstqst_sale_gst_en" model="account.tax.template"> @@ -105,6 +119,8 @@ <field name="account_collected_id" ref="chart2131_en"/> <field name="account_paid_id" ref="chart2131_en"/> <field name="parent_id" ref="gstqst_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="qst_sale_en" model="account.tax.template"> @@ -118,6 +134,8 @@ <field name="account_collected_id" ref="chart2132_en"/> <field name="account_paid_id" ref="chart2132_en"/> <field name="parent_id" ref="gstqst_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- Saskatchewan PST --> @@ -130,6 +148,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstpst_sale_sk_gst_en" model="account.tax.template"> @@ -144,6 +164,8 @@ <field name="account_collected_id" ref="chart2131_en"/> <field name="account_paid_id" ref="chart2131_en"/> <field name="parent_id" ref="gstpst_sk_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="pst_sk_sale_en" model="account.tax.template"> @@ -157,6 +179,8 @@ <field name="account_collected_id" ref="chart2132_en"/> <field name="account_paid_id" ref="chart2132_en"/> <field name="parent_id" ref="gstpst_sk_sale_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- HST --> @@ -170,6 +194,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart21331_en"/> <field name="account_paid_id" ref="chart21331_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="hst14_sale_en" model="account.tax.template"> @@ -181,6 +207,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart21332_en"/> <field name="account_paid_id" ref="chart21332_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="hst15_sale_en" model="account.tax.template"> @@ -192,6 +220,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart21333_en"/> <field name="account_paid_id" ref="chart21333_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- GST --> @@ -205,6 +235,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart2131_en"/> <field name="account_paid_id" ref="chart2131_en"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- PURCHASE TAXES --> @@ -219,6 +251,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend">1</field> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstpst_purc_bc_gst_en" model="account.tax.template"> @@ -233,6 +267,8 @@ <field name="account_collected_id" ref="chart1181_en"/> <field name="account_paid_id" ref="chart1181_en"/> <field name="parent_id" ref="gstpst_bc_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="pst_bc_purc_en" model="account.tax.template"> @@ -246,6 +282,8 @@ <field name="account_collected_id" ref="chart1182_en"/> <field name="account_paid_id" ref="chart1182_en"/> <field name="parent_id" ref="gstpst_bc_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- Manitoba PST --> @@ -258,6 +296,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstpst_purc_mb_gst_en" model="account.tax.template"> @@ -272,6 +312,8 @@ <field name="account_collected_id" ref="chart1181_en"/> <field name="account_paid_id" ref="chart1181_en"/> <field name="parent_id" ref="gstpst_mb_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="pst_mb_purc_en" model="account.tax.template"> @@ -285,6 +327,8 @@ <field name="account_collected_id" ref="chart1182_en"/> <field name="account_paid_id" ref="chart1182_en"/> <field name="parent_id" ref="gstpst_mb_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- Quebec PST --> @@ -297,6 +341,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstqst_purc_gst_en" model="account.tax.template"> @@ -310,6 +356,8 @@ <field name="account_collected_id" ref="chart1181_en"/> <field name="account_paid_id" ref="chart1181_en"/> <field name="parent_id" ref="gstqst_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="qst_purc_en" model="account.tax.template"> @@ -323,6 +371,8 @@ <field name="account_collected_id" ref="chart1182_en"/> <field name="account_paid_id" ref="chart1182_en"/> <field name="parent_id" ref="gstqst_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- Saskatchewan PST --> @@ -335,6 +385,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstpst_purc_sk_gst_en" model="account.tax.template"> @@ -349,6 +401,8 @@ <field name="account_collected_id" ref="chart1181_en"/> <field name="account_paid_id" ref="chart1181_en"/> <field name="parent_id" ref="gstpst_sk_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="pst_sk_purc_en" model="account.tax.template"> @@ -362,6 +416,8 @@ <field name="account_collected_id" ref="chart1182_en"/> <field name="account_paid_id" ref="chart1182_en"/> <field name="parent_id" ref="gstpst_sk_purc_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- HST --> @@ -375,6 +431,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart11831_en"/> <field name="account_paid_id" ref="chart11831_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="hst14_purc_en" model="account.tax.template"> @@ -386,6 +444,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart11832_en"/> <field name="account_paid_id" ref="chart11832_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="hst15_purc_en" model="account.tax.template"> @@ -397,6 +457,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart11833_en"/> <field name="account_paid_id" ref="chart11833_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- GST --> @@ -410,6 +472,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart1181_en"/> <field name="account_paid_id" ref="chart1181_en"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> </data> diff --git a/addons/l10n_ca/account_tax_fr.xml b/addons/l10n_ca/account_tax_fr.xml index 793bb1c750e3ee427b72b1c5a753647f96aa3306..2a72eb1af772be14cde45058224b15bdb87d0d82 100644 --- a/addons/l10n_ca/account_tax_fr.xml +++ b/addons/l10n_ca/account_tax_fr.xml @@ -14,6 +14,8 @@ <field name="amount">1</field> <field name="child_depend">1</field> <field name="type">percent</field> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstpst_sale_bc_gst_fr" model="account.tax.template"> @@ -28,6 +30,8 @@ <field name="account_collected_id" ref="chart2131_fr"/> <field name="account_paid_id" ref="chart2131_fr"/> <field name="parent_id" ref="gstpst_bc_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="pst_bc_sale_fr" model="account.tax.template"> @@ -41,6 +45,8 @@ <field name="account_collected_id" ref="chart2132_fr"/> <field name="account_paid_id" ref="chart2132_fr"/> <field name="parent_id" ref="gstpst_bc_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- Manitoba PST --> @@ -53,6 +59,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstpst_sale_mb_gst_fr" model="account.tax.template"> @@ -67,6 +75,8 @@ <field name="account_collected_id" ref="chart2131_fr"/> <field name="account_paid_id" ref="chart2131_fr"/> <field name="parent_id" ref="gstpst_mb_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="pst_mb_sale_fr" model="account.tax.template"> @@ -80,6 +90,8 @@ <field name="account_collected_id" ref="chart2132_fr"/> <field name="account_paid_id" ref="chart2132_fr"/> <field name="parent_id" ref="gstpst_mb_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- Quebec PST --> @@ -92,6 +104,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstqst_sale_gst_fr" model="account.tax.template"> @@ -105,6 +119,8 @@ <field name="account_collected_id" ref="chart2131_fr"/> <field name="account_paid_id" ref="chart2131_fr"/> <field name="parent_id" ref="gstqst_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="tvq_sale_fr" model="account.tax.template"> @@ -118,6 +134,8 @@ <field name="account_collected_id" ref="chart2132_fr"/> <field name="account_paid_id" ref="chart2132_fr"/> <field name="parent_id" ref="gstqst_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- Saskatchewan PST --> @@ -130,6 +148,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="gstpst_sale_sk_gst_fr" model="account.tax.template"> @@ -144,6 +164,8 @@ <field name="account_collected_id" ref="chart2131_fr"/> <field name="account_paid_id" ref="chart2131_fr"/> <field name="parent_id" ref="gstpst_sk_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="pst_sk_sale_fr" model="account.tax.template"> @@ -157,6 +179,8 @@ <field name="account_collected_id" ref="chart2132_fr"/> <field name="account_paid_id" ref="chart2132_fr"/> <field name="parent_id" ref="gstpst_sk_sale_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- HST --> @@ -170,6 +194,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart21331_fr"/> <field name="account_paid_id" ref="chart21331_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="hst14_sale_fr" model="account.tax.template"> @@ -181,6 +207,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart21332_fr"/> <field name="account_paid_id" ref="chart21332_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <record id="hst15_sale_fr" model="account.tax.template"> @@ -192,6 +220,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart21333_fr"/> <field name="account_paid_id" ref="chart21333_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> <!-- GST --> @@ -205,6 +235,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart2131_fr"/> <field name="account_paid_id" ref="chart2131_fr"/> + <field name="ref_base_sign" eval="-1"/> + <field name="ref_tax_sign" eval="-1"/> </record> @@ -220,6 +252,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend">1</field> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstpst_purc_bc_gst_fr" model="account.tax.template"> @@ -234,6 +268,8 @@ <field name="account_collected_id" ref="chart1181_fr"/> <field name="account_paid_id" ref="chart1181_fr"/> <field name="parent_id" ref="gstpst_bc_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="pst_bc_purc_fr" model="account.tax.template"> @@ -247,6 +283,8 @@ <field name="account_collected_id" ref="chart1182_fr"/> <field name="account_paid_id" ref="chart1182_fr"/> <field name="parent_id" ref="gstpst_bc_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- Manitoba PST --> @@ -259,6 +297,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstpst_purc_mb_gst_fr" model="account.tax.template"> @@ -273,6 +313,8 @@ <field name="account_collected_id" ref="chart1181_fr"/> <field name="account_paid_id" ref="chart1181_fr"/> <field name="parent_id" ref="gstpst_mb_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="pst_mb_purc_fr" model="account.tax.template"> @@ -286,6 +328,8 @@ <field name="account_collected_id" ref="chart1182_fr"/> <field name="account_paid_id" ref="chart1182_fr"/> <field name="parent_id" ref="gstpst_mb_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- Quebec PST --> @@ -298,6 +342,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstqst_purc_gst_fr" model="account.tax.template"> @@ -311,6 +357,8 @@ <field name="account_collected_id" ref="chart1181_fr"/> <field name="account_paid_id" ref="chart1181_fr"/> <field name="parent_id" ref="gstqst_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="tvq_purc_fr" model="account.tax.template"> @@ -324,6 +372,8 @@ <field name="account_collected_id" ref="chart1182_fr"/> <field name="account_paid_id" ref="chart1182_fr"/> <field name="parent_id" ref="gstqst_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- Saskatchewan PST --> @@ -336,6 +386,8 @@ <field name="amount">1</field> <field name="type">percent</field> <field name="child_depend" eval="True"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="gstpst_purc_sk_gst_fr" model="account.tax.template"> @@ -350,6 +402,8 @@ <field name="account_collected_id" ref="chart1181_fr"/> <field name="account_paid_id" ref="chart1181_fr"/> <field name="parent_id" ref="gstpst_sk_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="pst_sk_purc_fr" model="account.tax.template"> @@ -363,6 +417,8 @@ <field name="account_collected_id" ref="chart1182_fr"/> <field name="account_paid_id" ref="chart1182_fr"/> <field name="parent_id" ref="gstpst_sk_purc_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- HST --> @@ -376,6 +432,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart11831_fr"/> <field name="account_paid_id" ref="chart11831_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="hst14_purc_fr" model="account.tax.template"> @@ -387,6 +445,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart11832_fr"/> <field name="account_paid_id" ref="chart11832_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <record id="hst15_purc_fr" model="account.tax.template"> @@ -398,6 +458,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart11833_fr"/> <field name="account_paid_id" ref="chart11833_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> <!-- GST --> @@ -411,6 +473,8 @@ <field name="type">percent</field> <field name="account_collected_id" ref="chart1181_fr"/> <field name="account_paid_id" ref="chart1181_fr"/> + <field name="base_sign" eval="-1"/> + <field name="tax_sign" eval="-1"/> </record> </data> diff --git a/addons/l10n_fr/report/base_report.py b/addons/l10n_fr/report/base_report.py index 8e9c75161e1e1e9e21bd41aae9d08b33720f4a5f..d38dcd5c007d5390fdf46ca0bf21e0dc75aad692 100644 --- a/addons/l10n_fr/report/base_report.py +++ b/addons/l10n_fr/report/base_report.py @@ -28,6 +28,7 @@ import time from openerp.report import report_sxw +from openerp.tools.safe_eval import safe_eval as eval class base_report(report_sxw.rml_parse): diff --git a/addons/l10n_jp/__init__.py b/addons/l10n_jp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..03d6a3a323c9c4fef9dad27dfeb0ffdb6a4812a2 --- /dev/null +++ b/addons/l10n_jp/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright (C) Rooms For (Hong Kong) Limited T/A OSCG +# +# 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 <http://www.gnu.org/licenses/>. + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/l10n_jp/__openerp__.py b/addons/l10n_jp/__openerp__.py new file mode 100644 index 0000000000000000000000000000000000000000..85acc113fc5ce267b846258b0e3038019350432a --- /dev/null +++ b/addons/l10n_jp/__openerp__.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Copyright (C) Rooms For (Hong Kong) Limited T/A OSCG +# +# 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 <http://www.gnu.org/licenses/>. + +{ + 'name': 'Japan - Accounting', + 'version': '1.2', + 'category': 'Localization/Account Charts', + 'description': """ + +Overview: +--------- + +* Chart of Accounts and Taxes template for companies in Japan. +* This probably does not cover all the necessary accounts for a company. \ +You are expected to add/delete/modify accounts based on this template. + +Note: +----- + +* Fiscal positions '内税' and '外税' have been added to handle special \ +requirements which might arise from POS implementation. [1] You may not \ +need to use these at all under normal circumstances. + +[1] See https://github.com/odoo/odoo/pull/6470 for detail. + + """, + 'author': 'Rooms For (Hong Kong) Limited T/A OSCG', + 'website': 'http://www.openerp-asia.net/', + 'depends': ['account'], + 'data': [ + 'data/account.account.template.csv', + 'data/account.tax.code.template.csv', + 'data/account.chart.template.csv', + 'data/account.tax.template.csv', + 'data/account.fiscal.position.template.csv', + ], + 'installable': False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/l10n_jp/data/account.account.template.csv b/addons/l10n_jp/data/account.account.template.csv new file mode 100644 index 0000000000000000000000000000000000000000..06a617863513a6f7f0801a5b7a017e407a90f32d --- /dev/null +++ b/addons/l10n_jp/data/account.account.template.csv @@ -0,0 +1,157 @@ +id,code,name,parent_id:id,type,user_type/id,tax_ids/id,reconcile,currency_id/id +COM0,0,日本勘定科目表,,View,account.data_account_type_view,,FALSE, +A,A,貸借対照表,COM0,View,account.data_account_type_view,,FALSE, +A1,A1,資産ã®éƒ¨,A,View,account.account_type_asset_view1,,FALSE, +A11,A11,æµå‹•è³‡ç”£,A1,View,account.account_type_asset_view1,,FALSE, +A111,A111,ç¾é‡‘・é 金,A11,View,account.account_type_asset_view1,,FALSE, +A11101,A11101,普通é 金,A111,Liquidity,account.data_account_type_cash,,FALSE, +A11102,A11102,当座é 金,A111,Liquidity,account.data_account_type_cash,,FALSE, +A11103,A11103,定期é 金,A111,Liquidity,account.data_account_type_cash,,FALSE, +A11105,A11105,å°å£ç¾é‡‘,A111,Liquidity,account.data_account_type_cash,,FALSE, +A112,A112,売掛金ç‰,A11,View,account.account_type_asset_view1,,FALSE, +A11211,A11211,売掛金,A112,Receivable,account.data_account_type_receivable,,TRUE, +A11212,A11212,売掛金(USD),A112,Receivable,account.data_account_type_receivable,,TRUE,base.USD +A11215,A11215,å—å–手形,A112,Receivable,account.data_account_type_receivable,,TRUE, +A11216,A11216,割引手形,A112,Receivable,account.data_account_type_receivable,,TRUE, +A11219,A11219,出庫請求仮,A112,Regular,account.data_account_type_receivable,,FALSE, +A11301,A11301,未åŽåŽç›Š,A11,Regular,account.data_account_type_asset,,FALSE, +A11401,A11401,有価証券,A11,Regular,account.data_account_type_asset,,FALSE, +A115,A115,在庫,A11,View,account.account_type_asset_view1,,FALSE, +A11501,A11501,商å“,A115,Regular,account.data_account_type_asset,,FALSE, +A11503,A11503,製å“,A115,Regular,account.data_account_type_asset,,FALSE, +A11504,A11504,仕掛å“,A115,Regular,account.data_account_type_asset,,FALSE, +A11505,A11505,原ææ–™,A115,Regular,account.data_account_type_asset,,FALSE, +A11506,A11506,資æ,A115,Regular,account.data_account_type_asset,,FALSE, +A11503,A11503,ç©é€å“,A115,Regular,account.data_account_type_asset,,FALSE, +A11601,A11601,未åŽé‡‘,A11,Receivable,account.data_account_type_receivable,,TRUE, +A118,A118,ãã®ä»–æµå‹•è³‡ç”£,A11,View,account.account_type_asset_view1,,FALSE, +A11802,A11802,å‰æ¸¡é‡‘,A118,Regular,account.data_account_type_asset,,FALSE, +A11804,A11804,å‰æ‰•è²»ç”¨,A118,Regular,account.data_account_type_asset,,FALSE, +A11805,A11805,立替金,A118,Regular,account.data_account_type_asset,,FALSE, +A11806,A11806,仮払金,A118,Regular,account.data_account_type_asset,,FALSE, +A11807,A11807,仮払消費税,A118,Regular,account.conf_account_type_tax,,FALSE, +A11901,A11901,貸倒引当金,A11,Regular,account.data_account_type_asset,,FALSE, +A12,A12,固定資産,A1,View,account.account_type_asset_view1,,FALSE, +A121,A121,有形固定資産,A12,View,account.account_type_asset_view1,,FALSE, +A1211,A1211,建物åŠã³æ§‹ç¯‰ç‰©(ç´”é¡),A121,View,account.account_type_asset_view1,,FALSE, +A12111,A12111,建物åŠã³æ§‹ç¯‰ç‰©,A1211,Regular,account.data_account_type_asset,,FALSE, +A12112,A12112,建物åŠã³æ§‹ç¯‰ç‰©æ¸›å„Ÿç´¯è¨ˆé¡,A1211,Regular,account.data_account_type_asset,,FALSE, +A1212,A1212,機械åŠã³è£…ç½®(ç´”é¡),A121,View,account.account_type_asset_view1,,FALSE, +A12121,A12121,機械åŠã³è£…ç½®,A1212,Regular,account.data_account_type_asset,,FALSE, +A12122,A12122,機械åŠã³è£…置物減償累計é¡,A1212,Regular,account.data_account_type_asset,,FALSE, +A1213,A1213,工具ã€æ©Ÿå™¨åŠã³å‚™å“(ç´”é¡),A121,View,account.account_type_asset_view1,,FALSE, +A12131,A12131,工具ã€æ©Ÿå™¨åŠã³å‚™å“,A1213,Regular,account.data_account_type_asset,,FALSE, +A12132,A12132,工具ã€æ©Ÿå™¨åŠã³å‚™å“減償累計é¡,A1213,Regular,account.data_account_type_asset,,FALSE, +A12141,A12141,土地,A121,Regular,account.data_account_type_asset,,FALSE, +A122,A122,無形固定資産,A12,View,account.account_type_asset_view1,,FALSE, +A1221,A1221,ソフトウェア,A122,Regular,account.data_account_type_asset,,FALSE, +A1222,A1222,ソフトウェア仮勘定,A122,Regular,account.data_account_type_asset,,FALSE, +A1223,A1223,ãã®ä»–無形固定資産,A122,Regular,account.data_account_type_asset,,FALSE, +A123,A123,投資ãã®ä»–資産,A12,View,account.account_type_asset_view1,,FALSE, +A12301,A12301,投資有価証券,A123,Regular,account.data_account_type_asset,,FALSE, +A12302,A12302,é–¢ä¿‚ä¼šç¤¾æ ªå¼,A123,Regular,account.data_account_type_asset,,FALSE, +A12303,A12303,長期貸付金,A123,Regular,account.data_account_type_asset,,FALSE, +A13,A13,繰延資産,A1,View,account.account_type_asset_view1,,FALSE, +A13001,A13001,創立費,A13,Regular,account.data_account_type_asset,,FALSE, +A2,A2,è² å‚µã®éƒ¨,A,View,account.account_type_liability_view1,,FALSE, +A21,A21,æµå‹•è² 債,A2,View,account.account_type_liability_view1,,FALSE, +A212,A212,買掛金ç‰,A21,View,account.account_type_liability_view1,,FALSE, +A21211,A21211,買掛金,A212,Payable,account.data_account_type_payable,,TRUE, +A21212,A21212,買掛金(USD),A212,Payable,account.data_account_type_payable,,TRUE,base.USD +A21215,A21215,支払手形,A212,Payable,account.data_account_type_payable,,TRUE, +A21219,A21219,入庫請求仮,A212,Regular,account.data_account_type_payable,,FALSE, +A213,A213,çŸæœŸå€Ÿå…¥é‡‘ç‰,A212,Payable,account.data_account_type_payable,,TRUE, +A21301,A21301,çŸæœŸå€Ÿå…¥é‡‘,A21,Payable,account.data_account_type_payable,,TRUE, +A21302,A21302,一年以内返済長期借入金,A21,Payable,account.data_account_type_payable,,TRUE, +A214,A214,未払金ç‰,A21,View,account.account_type_liability_view1,,FALSE, +A21401,A21401,未払金,A214,Payable,account.data_account_type_payable,,TRUE, +A21404,A21404,未払金(クレジット),A214,Payable,account.data_account_type_payable,,TRUE, +A21407,A21407,未払é…当金,A214,Payable,account.data_account_type_payable,,TRUE, +A21501,A21501,未払費用,A21,Regular,account.data_account_type_liability,,FALSE, +A21601,A21601,未払法人税ç‰,A21,Regular,account.conf_account_type_tax,,FALSE, +A218,A218,ãã®ä»–æµå‹•è² 債,A21,View,account.account_type_liability_view1,,FALSE, +A21802,A21802,å‰å—金,A218,Regular,account.data_account_type_liability,,FALSE, +A21803,A21803,é り金,A218,Regular,account.data_account_type_liability,,FALSE, +A21804,A21804,ä»®å—金,A218,Regular,account.data_account_type_liability,,FALSE, +A21806,A21806,賞与引当金,A218,Regular,account.data_account_type_liability,,FALSE, +A21807,A21807,ä»®å—消費税,A218,Regular,account.conf_account_type_tax,,FALSE, +A22,A22,å›ºå®šè² å‚µ,A2,View,account.account_type_liability_view1,,FALSE, +A22001,A22001,社債,A22,Regular,account.data_account_type_liability,,FALSE, +A22002,A22002,長期借入金,A22,Regular,account.data_account_type_liability,,FALSE, +A22005,A22005,退è·çµ¦ä»˜å¼•å½“金,A22,Regular,account.data_account_type_liability,,FALSE, +A3,A3,純資産ã®éƒ¨,A,View,account.account_type_liability_view1,,FALSE, +A31,A31,æ ªä¸»è³‡æœ¬,A3,View,account.account_type_liability_view1,,FALSE, +A31001,A31001,資本金,A31,Regular,account.conf_account_type_equity,,FALSE, +A32,A32,資本剰余金,A3,View,account.account_type_liability_view1,,FALSE, +A32101,A32101,資本準備金,A32,Regular,account.conf_account_type_equity,,FALSE, +A32109,A32109,ãã®ä»–資本剰余金,A32,Regular,account.conf_account_type_equity,,FALSE, +A33,A33,利益剰余金,A3,View,account.account_type_liability_view1,,FALSE, +A33101,A33101,利益準備金,A33,Regular,account.conf_account_type_equity,,FALSE, +A332,A332,ãã®ä»–利益剰余金,A33,View,account.account_type_liability_view1,,FALSE, +A33202,A33202,特別償å´æº–備金,A332,Regular,account.conf_account_type_equity,,FALSE, +A33204,A33204,ä»»æ„ç©ç«‹é‡‘,A332,Regular,account.conf_account_type_equity,,FALSE, +A33209,A33209,繰越利益剰余金,A332,Regular,account.conf_account_type_equity,,FALSE, +B,B,æ益計算書,COM0,View,account.data_account_type_view,,FALSE, +B4,B4,売上ç·åˆ©ç›Š,B,View,account.account_type_income_view1,,FALSE, +B41001,B41001,売上高,B4,Regular,account.data_account_type_income,,FALSE, +B42001,B42001,売上原価,B4,Regular,account.data_account_type_income,,FALSE, +B42091,B42091,è³¼è²·ä¾¡æ ¼å·®ç•°,B4,Regular,account.data_account_type_income,,FALSE, +B42092,B42092,棚å¸èª¿æ•´,B4,Regular,account.data_account_type_income,,FALSE, +B5,B5,販売費åŠã³ä¸€èˆ¬ç®¡ç†è²»,B,View,account.account_type_expense_view1,,FALSE, +B50011,B50011,貸倒引当金繰入é¡,B5,Regular,account.data_account_type_expense,,FALSE, +B50041,B50041,å½¹å“¡å ±é…¬,B5,Regular,account.data_account_type_expense,,FALSE, +B50071,B50071,給料åŠã³æ‰‹å½“,B5,Regular,account.data_account_type_expense,,FALSE, +B50081,B50081,賞与,B5,Regular,account.data_account_type_expense,,FALSE, +B50091,B50091,退è·é‡‘,B5,Regular,account.data_account_type_expense,,FALSE, +B50111,B50111,賞与引当金繰入,B5,Regular,account.data_account_type_expense,,FALSE, +B50131,B50131,退è·çµ¦ä»˜å¼•å½“金繰入,B5,Regular,account.data_account_type_expense,,FALSE, +B50151,B50151,法定ç¦åˆ©è²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50161,B50161,ç¦åˆ©åŽšç”Ÿè²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50171,B50171,ä¿é™ºæ–™,B5,Regular,account.data_account_type_expense,,FALSE, +B50191,B50191,外注費,B5,Regular,account.data_account_type_expense,,FALSE, +B50211,B50211,è·é€ é‹è³ƒ,B5,Regular,account.data_account_type_expense,,FALSE, +B50221,B50221,棚å¸æ¸›è€—è²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50231,B50231,商å“評価æ,B5,Regular,account.data_account_type_expense,,FALSE, +B50241,B50241,地代家賃,B5,Regular,account.data_account_type_expense,,FALSE, +B50251,B50251,リース料,B5,Regular,account.data_account_type_expense,,FALSE, +B50271,B50271,広告宣ä¼è²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50281,B50281,通信費,B5,Regular,account.data_account_type_expense,,FALSE, +B50311,B50311,消耗å“è²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50341,B50341,旅費交通費,B5,Regular,account.data_account_type_expense,,FALSE, +B50371,B50371,交際費,B5,Regular,account.data_account_type_expense,,FALSE, +B50401,B50401,支払手数料,B5,Regular,account.data_account_type_expense,,FALSE, +B50431,B50431,諸会費,B5,Regular,account.data_account_type_expense,,FALSE, +B50461,B50461,æ–°èžå›³æ›¸è²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50491,B50491,租税公課,B5,Regular,account.data_account_type_expense,,FALSE, +B50511,B50511,æ°´é“光熱費,B5,Regular,account.data_account_type_expense,,FALSE, +B50521,B50521,会è°è²»,B5,Regular,account.data_account_type_expense,,FALSE, +B50551,B50551,雑費,B5,Regular,account.data_account_type_expense,,FALSE, +B61,B61,å–¶æ¥å¤–åŽç›Š,B,View,account.account_type_income_view1,,FALSE, +B61101,B61101,å—å–利æ¯,B61,Regular,account.data_account_type_income,,FALSE, +B61201,B61201,å—å–é…当金,B61,Regular,account.data_account_type_income,,FALSE, +B61401,B61401,未実ç¾ç‚ºæ›¿å·®ç›Š,B61,Regular,account.data_account_type_income,,FALSE, +B61501,B61501,実ç¾ç‚ºæ›¿å·®ç›Š,B61,Regular,account.data_account_type_income,,FALSE, +B61601,B61601,投資有価証券売å´ç›Š(外益),B61,Regular,account.data_account_type_income,,FALSE, +B61602,B61602,投資有価証券評価益(外益),B61,Regular,account.data_account_type_income,,FALSE, +B61801,B61801,雑åŽå…¥,B61,Regular,account.data_account_type_income,,FALSE, +B62,B62,å–¶æ¥å¤–費用,B,View,account.account_type_expense_view1,,FALSE, +B62101,B62101,支払利æ¯,B62,Regular,account.data_account_type_expense,,FALSE, +B62201,B62201,創立費償å´,B62,Regular,account.data_account_type_expense,,FALSE, +B62401,B62401,未実ç¾ç‚ºæ›¿å·®æ,B62,Regular,account.data_account_type_expense,,FALSE, +B62402,B62402,実ç¾ç‚ºæ›¿å·®æ,B62,Regular,account.data_account_type_expense,,FALSE, +B62601,B62601,投資有価証券売å´æ(外æ),B62,Regular,account.data_account_type_expense,,FALSE, +B62602,B62602,投資有価証券評価æ(外æ),B62,Regular,account.data_account_type_expense,,FALSE, +B62801,B62801,雑費用,B62,Regular,account.data_account_type_expense,,FALSE, +B71,B71,特別利益,B,View,account.account_type_income_view1,,FALSE, +B71101,B71101,固定資産売å´ç›Š,B71,Regular,account.data_account_type_income,,FALSE, +B71102,B71102,固定資産評価益,B71,Regular,account.data_account_type_income,,FALSE, +B71201,B71201,投資有価証券売å´ç›Š(特益),B71,Regular,account.data_account_type_income,,FALSE, +B71202,B71202,投資有価証券評価益(特益),B71,Regular,account.data_account_type_income,,FALSE, +B72,B72,特別æ失,B,View,account.account_type_expense_view1,,FALSE, +B72101,B72101,固定資産売å´æ,B72,Regular,account.data_account_type_expense,,FALSE, +B72102,B72102,固定資産評価æ,B72,Regular,account.data_account_type_expense,,FALSE, +B72201,B72201,投資有価証券売å´æ(特æ),B72,Regular,account.data_account_type_expense,,FALSE, +B72202,B72202,投資有価証券評価æ(特æ),B72,Regular,account.data_account_type_expense,,FALSE, +B8,B8,法人税ç‰,B,View,account.account_type_expense_view1,,FALSE, +B80101,B80101,法人税ã€ä½æ°‘税åŠã³äº‹æ¥ç¨Ž,B8,Regular,account.data_account_type_expense,,FALSE, +B90001,B90001,期間利益,B,Regular,account.data_account_type_expense,,FALSE, diff --git a/addons/l10n_jp/data/account.chart.template.csv b/addons/l10n_jp/data/account.chart.template.csv new file mode 100644 index 0000000000000000000000000000000000000000..8488d0445aa1bd13f056ef7436885c175fb5e7e5 --- /dev/null +++ b/addons/l10n_jp/data/account.chart.template.csv @@ -0,0 +1,2 @@ +id,name,account_root_id/id,tax_code_root_id/id,bank_account_view_id/id,property_account_receivable/id,property_account_payable/id,property_account_expense_categ/id,property_account_income_categ/id,property_account_expense/id,property_account_income/id +l10n_jp1,日本勘定è¨å®šãƒ†ãƒ³ãƒ—レート,COM0,jpt_root,A111,A11211,A21211,A21219,B41001,A21219,B41001 diff --git a/addons/l10n_jp/data/account.fiscal.position.template.csv b/addons/l10n_jp/data/account.fiscal.position.template.csv new file mode 100644 index 0000000000000000000000000000000000000000..07e7b11531da65a0cd82fd044e66fad537173f08 --- /dev/null +++ b/addons/l10n_jp/data/account.fiscal.position.template.csv @@ -0,0 +1,7 @@ +id,chart_template_id/id,name,note,tax_ids/tax_src_id/id,tax_ids/tax_dest_id/id +fiscal_position_tax_inclusive_template,l10n_jp1,内税,,tax_in_e,tax_in_i +,,,,tax_out_e,tax_out_i +fiscal_position_tax_exclusive_template,l10n_jp1,外税,,tax_in_i,tax_in_e +,,,,tax_out_i,tax_out_e +fiscal_position_tax_exempt_template,l10n_jp1,海外å–引先,,tax_in_e,tax_in_x +,,,,tax_out_e,tax_out_im diff --git a/addons/l10n_jp/data/account.tax.code.template.csv b/addons/l10n_jp/data/account.tax.code.template.csv new file mode 100644 index 0000000000000000000000000000000000000000..71bf73cff475841a6ab8c08de4aaece6cd2fc366 --- /dev/null +++ b/addons/l10n_jp/data/account.tax.code.template.csv @@ -0,0 +1,20 @@ +id,name,parent_id/id,code,notprintable,sign +jpt_root,日本税ãƒãƒ£ãƒ¼ãƒˆ,,,FALSE,1 +jpt_tax_code_balance_net,支払対象税é¡,jpt_root,,FALSE,1 +jpt_tax_code_input,ä»®å—税é¡,jpt_tax_code_balance_net,,FALSE,-1 +jpt_tax_code_input_A,ä»®å—消費税(8%),jpt_tax_code_input,,FALSE,1 +jpt_tax_code_input_X,å…税,jpt_tax_code_input,,FALSE,1 +jpt_tax_code_input_O,ä¸èª²ç¨Ž,jpt_tax_code_input,,FALSE,1 +jpt_tax_code_output,仮払税é¡,jpt_tax_code_balance_net,,FALSE,1 +jpt_tax_code_output_A,仮払消費税(8%),jpt_tax_code_output,,FALSE,1 +jpt_tax_code_output_I,輸入,jpt_tax_code_output,,FALSE,1 +jpt_tax_code_output_O,ä¸èª²ç¨Ž,jpt_tax_code_output,,FALSE,1 +jpt_tax_code_base_net,税計算基準é¡,jpt_root,,FALSE,1 +jpt_tax_code_base_sales,販売基準é¡,jpt_tax_code_base_net,,FALSE,1 +jpt_tax_code_sales_A,課税対象売上(8%),jpt_tax_code_base_sales,,FALSE,1 +jpt_tax_code_sales_X,å…税売上,jpt_tax_code_base_sales,,FALSE,1 +jpt_tax_code_sales_O,ä¸èª²ç¨Žå£²ä¸Š,jpt_tax_code_base_sales,,FALSE,1 +jpt_tax_code_base_purchases,購買基準é¡,jpt_tax_code_base_net,,FALSE,1 +jpt_tax_code_purch_A,課税対象仕入(8%),jpt_tax_code_base_purchases,,FALSE,1 +jpt_tax_code_purch_I,輸入仕入,jpt_tax_code_base_purchases,,FALSE,1 +jpt_tax_code_purch_O,ä¸èª²ç¨Žä»•å…¥,jpt_tax_code_base_purchases,,FALSE,1 diff --git a/addons/l10n_jp/data/account.tax.template.csv b/addons/l10n_jp/data/account.tax.template.csv new file mode 100644 index 0000000000000000000000000000000000000000..d029c0d41e0d12a058f4b676e18277e278c2ce3d --- /dev/null +++ b/addons/l10n_jp/data/account.tax.template.csv @@ -0,0 +1,9 @@ +id,name,description,chart_template_id/id,type_tax_use,type,amount,price_include,sequence,include_base_amount,account_collected_id/id,base_code_id/id,base_sign,tax_code_id/id,tax_sign,account_paid_id/id,ref_base_code_id/id,ref_base_sign,ref_tax_code_id/id,ref_tax_sign,child_depend +tax_in_e,ä»®å—消費税(外),,l10n_jp1,sale,percent,0.08,FALSE,1,FALSE,A11807,jpt_tax_code_sales_A,1,jpt_tax_code_input_A,1,A11807,jpt_tax_code_sales_A,1,jpt_tax_code_input_A,1,FALSE +tax_in_i,ä»®å—消費税(内),,l10n_jp1,sale,percent,0.08,TRUE,1,FALSE,A11807,jpt_tax_code_sales_A,1,jpt_tax_code_input_A,1,A11807,jpt_tax_code_sales_A,1,jpt_tax_code_input_A,1,FALSE +tax_in_x,輸出å…税,,l10n_jp1,sale,percent,,FALSE,1,FALSE,,jpt_tax_code_sales_X,1,jpt_tax_code_input_X,1,,jpt_tax_code_sales_X,1,jpt_tax_code_input_X,1,FALSE +tax_in_o,éžèª²ç¨Žè²©å£²,,l10n_jp1,sale,percent,,FALSE,1,FALSE,,jpt_tax_code_sales_O,1,jpt_tax_code_input_O,1,,jpt_tax_code_sales_O,1,jpt_tax_code_input_O,1,FALSE +tax_out_e,仮払消費税(外),,l10n_jp1,purchase,percent,0.08,FALSE,1,FALSE,A21807,jpt_tax_code_purch_A,1,jpt_tax_code_output_A,1,A21807,jpt_tax_code_purch_A,1,jpt_tax_code_output_A,1,FALSE +tax_out_i,仮払消費税(内),,l10n_jp1,purchase,percent,0.08,TRUE,1,FALSE,A21807,jpt_tax_code_purch_A,1,jpt_tax_code_output_A,1,A21807,jpt_tax_code_purch_A,1,jpt_tax_code_output_A,1,FALSE +tax_out_im,海外仕入,,l10n_jp1,purchase,percent,,FALSE,1,FALSE,,jpt_tax_code_purch_I,1,jpt_tax_code_output_I,1,,jpt_tax_code_purch_I,1,jpt_tax_code_output_I,1,FALSE +tax_out_o,éžèª²ç¨Žè³¼è²·,,l10n_jp1,purchase,percent,,FALSE,1,FALSE,,jpt_tax_code_purch_O,1,jpt_tax_code_output_O,1,,jpt_tax_code_purch_O,1,jpt_tax_code_output_O,1,FALSE diff --git a/addons/l10n_jp/static/description/icon.png b/addons/l10n_jp/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..96bc158944ecb44e2e5afea17d0ba19216fe775a Binary files /dev/null and b/addons/l10n_jp/static/description/icon.png differ diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index 3136174a6570fdbf3b00c2e551498d483a2b3e0c..56844ad524345ee1b9484c17a8567026446319f8 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -8,6 +8,7 @@ from openerp import _, api, fields, models, SUPERUSER_ID from openerp.exceptions import UserError from openerp.modules.registry import RegistryManager from openerp.tools import ustr +from openerp.tools.safe_eval import safe_eval as eval _logger = logging.getLogger(__name__) diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index 441caadbccceabe04f9bed3da19a11a6166989a7..db1c20e8f0016324d36295dbb50349b22268d0ff 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -181,6 +181,7 @@ class Notification(models.Model): mail_values = { 'mail_message_id': message.id, 'auto_delete': self._context.get('mail_auto_delete', True), + 'mail_server_id': self._context.get('mail_server_id', False), 'body_html': body_html, 'recipient_ids': [(4, id) for id in chunk], 'references': references, diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index bb010372025c2c1588123bb35a36701efc285db1..7e191f9517da1c2411f2874a9520dda96e534a3b 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -17,7 +17,9 @@ def decode(text): # TDE proposal: move to tools ? if text: text = decode_header(text.replace('\r', '')) - return ''.join([tools.ustr(x[0], x[1]) for x in text]) + # The joining space will not be needed as of Python 3.3 + # See https://hg.python.org/cpython/rev/8c03fe231877 + return ' '.join([tools.ustr(x[0], x[1]) for x in text]) class Message(models.Model): diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 233598c33b1d47444992ffdd6ab1898c67a1e27e..734429607913aadd2853314b99164fa0d6c1b609 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -49,7 +49,7 @@ var TimelineRecordThread = form_common.AbstractField.extend ({ 'readonly': this.node.attrs.readonly || false, 'compose_placeholder' : this.node.attrs.placeholder || false, 'display_log_button' : this.options.display_log_button || true, - 'show_compose_message': this.view.is_action_enabled('edit') || false, + 'show_compose_message': true, 'show_link': this.parent.is_action_enabled('edit') || true, }, this.node.params); }, @@ -1434,7 +1434,6 @@ var ComposeMessage = Attachment.extend ({ } } } - this.$(".o_timeline_msg_attachment_list").html(this.display_attachments(this)); var $input = this.$('input.oe_form_binary_file'); diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 83af81104ec451bcb7aae172ce698b71fb485144..2a8957fa35f6b2c2fcfd1485aa64266c4256e035 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -216,7 +216,7 @@ class MailComposer(models.TransientModel): if wizard.template_id: # template user_signature is added when generating body_html # mass mailing: use template auto_delete value -> note, for emails mass mailing only - Mail = Mail.with_context(mail_notify_user_signature=False, mail_auto_delete=wizard.template_id.auto_delete) + Mail = Mail.with_context(mail_notify_user_signature=False, mail_auto_delete=wizard.template_id.auto_delete, mail_server_id=wizard.template_id.mail_server_id.id) if wizard.attachment_ids and wizard.composition_mode != 'mass_mail' and wizard.template_id: new_attachment_ids = [] for attachment in wizard.attachment_ids: @@ -285,7 +285,7 @@ class MailComposer(models.TransientModel): # static wizard (mail.message) values mail_values = { 'subject': self.subject, - 'body': self.body, + 'body': self.body or '', 'parent_id': self.parent_id and self.parent_id.id, 'partner_ids': [partner.id for partner in self.partner_ids], 'attachment_ids': [attach.id for attach in self.attachment_ids], diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index 7b929f5f24fe64f1a202a8924d1f94a44e94060b..2136110ae14e904c8bfbe2e28309c6a69fcc4212 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -19,9 +19,7 @@ # ############################################################################## -import openerp from openerp.osv import fields, osv -from openerp import models, api, _ class MailMailStats(osv.Model): @@ -35,28 +33,27 @@ class MailMailStats(osv.Model): _rec_name = 'message_id' _order = 'message_id' - def _compute_state(self, cr, uid, stat_ids, field_names, arg, context=None): - stats = self.browse(cr, uid, stat_ids, context=context) - res = dict([(stat, False) for stat in stat_ids]) + def _compute_state(self, cr, uid, ids, field_names, arg, context=None): + res = dict((i, {'state': 'outgoing', 'state_update': fields.datetime.now()}) for i in ids) - self.write(cr, uid, stat_ids, {'state_update': fields.datetime.now()}, context=context) - - for stat in stats: + for stat in self.browse(cr, uid, ids, context=context): if stat.exception: - res[stat.id] = 'exception' + res[stat.id]['state'] = 'exception' if stat.sent: - res[stat.id] = 'sent' + res[stat.id]['state'] = 'sent' if stat.opened: - res[stat.id] = 'opened' + res[stat.id]['state'] = 'opened' if stat.replied: - res[stat.id] = 'replied' + res[stat.id]['state'] = 'replied' if stat.bounced: - res[stat.id] = 'bounced' + res[stat.id]['state'] = 'bounced' return res + __store = {_name: ((lambda s, c, u, i, t: i), ['exception', 'sent', 'opened', 'replied', 'bounced'], 10)} + _columns = { - 'mail_mail_id': fields.many2one('mail.mail', 'Mail', ondelete='set null'), + 'mail_mail_id': fields.many2one('mail.mail', 'Mail', ondelete='set null', select=True), 'mail_mail_id_int': fields.integer( 'Mail ID (tech)', help='ID of the related mail_mail. This field is an integer field because' @@ -86,10 +83,17 @@ class MailMailStats(osv.Model): 'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'), 'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'), 'links_click_ids': fields.one2many('website.links.click', 'mail_stat_id', 'Links click'), - 'state': fields.function(_compute_state, string='State', type="selection", - selection=[('outgoing', 'Outgoing'), ('exception', 'Exception'), ('sent', 'Sent'), ('opened', 'Opened'), ('replied', 'Replied'), ('bounced', 'Bounced')], - store={'mail.mail.statistics': (lambda self, cr, uid, ids, context=None: ids, ['exception', 'sent', 'opened', 'replied', 'bounced'], 10)}), - 'state_update': fields.datetime('State Update', help='Last state update of the mail'), + 'state': fields.function(_compute_state, string='State', type="selection", multi="state", + selection=[('outgoing', 'Outgoing'), + ('exception', 'Exception'), + ('sent', 'Sent'), + ('opened', 'Opened'), + ('replied', 'Replied'), + ('bounced', 'Bounced')], + store=__store), + 'state_update': fields.function(_compute_state, string='State Update', type='datetime', + multi='state', help='Last state update of the mail', + store=__store), } _defaults = { diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 9528a5b808d0526b34d98cfe016b053e28944b4a..6b73230ca18b1e5774734e63bc46e3790597cf44 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -271,7 +271,7 @@ </record> <record model="ir.actions.act_window" id="action_view_mass_mailing_lists"> - <field name="name">Contact Lists</field> + <field name="name">Mailing Lists</field> <field name="res_model">mail.mass_mailing.list</field> <field name="view_type">form</field> <field name="view_mode">tree,form</field> diff --git a/addons/membership/membership_view.xml b/addons/membership/membership_view.xml index 34dcdaee642f373deb8888606a1698cb978c1129..00990346575d5c3378c30ec7d274e6497e2c3fd3 100644 --- a/addons/membership/membership_view.xml +++ b/addons/membership/membership_view.xml @@ -262,7 +262,7 @@ <field name="date"/> <field name="member_price"/> <field name="membership_id"/> - <field name="account_invoice_id"/> + <field name="account_invoice_id" context="{'form_view_ref': 'account.invoice_form'}"/> <field name="state" colspan="4"/> </form> </field> diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index 504c43e065dbd38181f218cc3cde474b88207d0b..1817d212669e22558d2fd5223ca5db2b4c3f8f05 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -25,7 +25,7 @@ from collections import OrderedDict import openerp.addons.decimal_precision as dp from openerp.osv import fields, osv from openerp.tools import DEFAULT_SERVER_DATE_FORMAT -from openerp.tools import float_compare +from openerp.tools import float_compare, float_is_zero from openerp.tools.translate import _ from openerp import tools, SUPERUSER_ID from openerp.addons.product import _common @@ -215,6 +215,8 @@ class mrp_bom(osv.osv): @param properties: List of related properties. @return: False or BoM id. """ + if not context: + context = {} if properties is None: properties = [] if product_id: @@ -232,19 +234,21 @@ class mrp_bom(osv.osv): else: # neither product nor template, makes no sense to search return False + if context.get('company_id'): + domain = domain + [('company_id', '=', context['company_id'])] domain = domain + [ '|', ('date_start', '=', False), ('date_start', '<=', time.strftime(DEFAULT_SERVER_DATE_FORMAT)), '|', ('date_stop', '=', False), ('date_stop', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT))] # order to prioritize bom with product_id over the one without - ids = self.search(cr, uid, domain, order='product_id', context=context) + ids = self.search(cr, uid, domain, order='sequence, product_id', context=context) # Search a BoM which has all properties specified, or if you can not find one, you could - # pass a BoM without any properties + # pass a BoM without any properties with the smallest sequence bom_empty_prop = False for bom in self.pool.get('mrp.bom').browse(cr, uid, ids, context=context): if not set(map(int, bom.property_ids or [])) - set(properties or []): - if properties and not bom.property_ids: - bom_empty_prop = bom.id - else: + if not properties or bom.property_ids: return bom.id + elif not bom_empty_prop: + bom_empty_prop = bom.id return bom_empty_prop def _bom_explode(self, cr, uid, bom, product, factor, properties=None, level=0, routing_id=False, previous_products=None, master_bom=None, context=None): @@ -300,6 +304,9 @@ class mrp_bom(osv.osv): if not product or (set(map(int,bom_line_id.attribute_value_ids or [])) - set(map(int,product.attribute_value_ids))): continue + if set(map(int, bom_line_id.property_ids or [])) - set(properties or []): + continue + if previous_products and bom_line_id.product_id.product_tmpl_id.id in previous_products: raise UserError(_('BoM "%s" contains a BoM line with a product recursion: "%s".') % (master_bom.name,bom_line_id.product_id.name_get()[0][1])) @@ -421,6 +428,7 @@ class mrp_bom_line(osv.osv): 'product_rounding': lambda *a: 0.0, 'type': lambda *a: 'normal', 'product_uom': _get_uom_id, + 'sequence': 1, } _sql_constraints = [ ('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n' \ @@ -602,7 +610,7 @@ class mrp_production(osv.osv): 'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), 'product_qty': lambda *a: 1.0, 'user_id': lambda self, cr, uid, c: uid, - 'name': lambda x, y, z, c: x.pool.get('ir.sequence').next_by_code(y, z, 'mrp.production') or '/', + 'name': lambda self, cr, uid, context: self.pool['ir.sequence'].next_by_code(cr, uid, 'mrp.production', context=context) or '/', 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'mrp.production', context=c), 'location_src_id': _src_id_default, 'location_dest_id': _dest_id_default @@ -745,7 +753,7 @@ class mrp_production(osv.osv): #reset workcenter_lines in production order for line in results2: line['production_id'] = production.id - workcenter_line_obj.create(cr, uid, line) + workcenter_line_obj.create(cr, uid, line, context) return results def action_compute(self, cr, uid, ids, properties=None, context=None): @@ -948,6 +956,7 @@ class mrp_production(osv.osv): uom_obj = self.pool.get("product.uom") production = self.browse(cr, uid, production_id, context=context) production_qty_uom = uom_obj._compute_qty(cr, uid, production.product_uom.id, production_qty, production.product_id.uom_id.id) + precision = self.pool['decimal.precision'].precision_get(cr, uid, 'Product Unit of Measure') main_production_move = False if production_mode == 'consume_produce': @@ -969,7 +978,8 @@ class mrp_production(osv.osv): location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context) stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context) remaining_qty = subproduct_factor * production_qty_uom - qty - if remaining_qty: # In case you need to make more than planned + if not float_is_zero(remaining_qty, precision_rounding=precision): + # In case you need to make more than planned #consumed more in wizard than previously planned extra_move_id = stock_mov_obj.copy(cr, uid, produce_product.id, default={'product_uom_qty': remaining_qty, 'production_id': production_id}, context=context) @@ -999,7 +1009,7 @@ class mrp_production(osv.osv): stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context) remaining_qty -= consumed_qty - if remaining_qty: + if not float_is_zero(remaining_qty, precision_rounding=precision): #consumed more in wizard than previously planned product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context) extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context) @@ -1242,6 +1252,8 @@ class mrp_production(osv.osv): """ Confirms production order. @return: Newly generated Shipment Id. """ + user_lang = self.pool.get('res.users').browse(cr, uid, [uid]).partner_id.lang + context = dict(context, lang=user_lang) uncompute_ids = filter(lambda x: x, [not x.product_lines and x.id or False for x in self.browse(cr, uid, ids, context=context)]) self.action_compute(cr, uid, uncompute_ids, context=context) for production in self.browse(cr, uid, ids, context=context): diff --git a/addons/mrp/mrp_demo.xml b/addons/mrp/mrp_demo.xml index 6732a5c3447ead09ab7128fb08861bf7feafe2c6..77b7ad4f2f6d67314c33cd4d6da2db3759a7bfbd 100644 --- a/addons/mrp/mrp_demo.xml +++ b/addons/mrp/mrp_demo.xml @@ -452,6 +452,25 @@ <field name="type">normal</field> </record> + <record id="mrp_bom_property_0" model="mrp.bom"> + <field name="name">PC Assemble + DDR 512MB</field> + <field name="product_tmpl_id" ref="product.product_product_3_product_template"/> + <field name="product_uom" ref="product.product_uom_unit"/> + <field name="sequence">5</field> + <field name="type">normal</field> + <field name="property_ids" eval="[(6,0,[ref('mrp_property_0')])]"/> + </record> + + <record id="mrp_bom_property_line" model="mrp.bom"> + <field name="name">RAM SR2 PLUS</field> + <field name="product_id" ref="product.product_product_14"/> + <field name="product_tmpl_id" ref="product.product_product_14_product_template"/> + <field name="product_uom" ref="product.product_uom_unit"/> + <field name="sequence">5</field> + <field name="type">phantom</field> + <field name="property_ids" eval="[(6,0,[ref('mrp_property_0')])]"/> + </record> + <record id="mrp_bom_line_17" model="mrp.bom.line"> <field name="product_id" ref="product.product_product_6"/> <field name="product_qty">1</field> @@ -501,7 +520,7 @@ <field name="name">PC Assemble + 512MB RAM</field> <field name="product_tmpl_id" ref="product.product_product_3_product_template"/> <field name="product_uom" ref="product.product_uom_unit"/> - <field name="sequence">5</field> + <field name="sequence">4</field> <field name="routing_id" ref="mrp_routing_2"/> <field name="type">phantom</field> </record> @@ -639,6 +658,35 @@ <field name="bom_id" ref="mrp_bom_11"/> </record> + <record id="mrp_bom_line_prop_1" model="mrp.bom.line"> + <field name="product_id" ref="product.product_product_13"/> + <field name="product_qty">1</field> + <field name="product_uom" ref="product.product_uom_unit"/> + <field name="sequence">1</field> + <field name="type">normal</field> + <field name="property_ids" eval="[(6,0,[ref('mrp_property_8')])]"/> + <field name="bom_id" ref="mrp_bom_property_line"/> + </record> + + <record id="mrp_bom_line_prop_2" model="mrp.bom.line"> + <field name="product_id" ref="product.product_product_12"/> + <field name="product_qty">1</field> + <field name="product_uom" ref="product.product_uom_unit"/> + <field name="sequence">1</field> + <field name="type">normal</field> + <field name="property_ids" eval="[(6,0,[ref('mrp_property_0')])]"/> + <field name="bom_id" ref="mrp_bom_property_line"/> + </record> + + <record id="mrp_bom_line_prop_3" model="mrp.bom.line"> + <field name="product_id" ref="product.product_product_11"/> + <field name="product_qty">1</field> + <field name="product_uom" ref="product.product_uom_unit"/> + <field name="sequence">1</field> + <field name="type">normal</field> + <field name="bom_id" ref="mrp_bom_property_line"/> + </record> + <record id="mrp_production_1" model="mrp.production"> <field name="product_id" ref="product.product_product_3"/> diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml index f89428be4f30e0938bdcf064074589c1044ee5a2..f2d2bf1253b6cbbfcace803e94669309a5db540f 100644 --- a/addons/mrp/mrp_view.xml +++ b/addons/mrp/mrp_view.xml @@ -400,6 +400,7 @@ <field name="date_start"/> <field name="date_stop"/> <field name="attribute_value_ids" widget="many2many_tags" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]"/> + <field name="property_ids" widget="many2many_tags"/> </tree> </field> </page> diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py index 7969a7f724a5d3027c0a98982052d37a43459afb..a0a9863491e47568215c03123d2179e9cee79fc3 100644 --- a/addons/mrp/procurement.py +++ b/addons/mrp/procurement.py @@ -87,7 +87,7 @@ class procurement_order(osv.osv): else: properties = [x.id for x in procurement.property_ids] bom_id = bom_obj._bom_find(cr, uid, product_id=procurement.product_id.id, - properties=properties, context=context) + properties=properties, context=dict(context, company_id=procurement.company_id.id)) bom = bom_obj.browse(cr, uid, bom_id, context=context) routing_id = bom.routing_id.id return { @@ -117,7 +117,7 @@ class procurement_order(osv.osv): if self.check_bom_exists(cr, uid, [procurement.id], context=context): #create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) vals = self._prepare_mo_vals(cr, uid, procurement, context=context) - produce_id = production_obj.create(cr, SUPERUSER_ID, vals, context=context) + produce_id = production_obj.create(cr, SUPERUSER_ID, vals, context=dict(context, force_company=procurement.company_id.id)) res[procurement.id] = produce_id self.write(cr, uid, [procurement.id], {'production_id': produce_id}) self.production_order_create_note(cr, uid, procurement, context=context) diff --git a/addons/mrp/stock.py b/addons/mrp/stock.py index 0aa91010c14b4429eaf756efe163922ba1fa2b91..77e3b0d27b6414c2d6cb25c21c7c787692e2e349 100644 --- a/addons/mrp/stock.py +++ b/addons/mrp/stock.py @@ -44,6 +44,7 @@ class StockMove(osv.osv): if move.raw_material_production_id and move.location_dest_id.usage == 'production' and move.raw_material_production_id.product_id.track_production and not move.consumed_for: raise UserError(_("Because the product %s requires it, you must assign a serial number to your raw material %s to proceed further in your production. Please use the 'Produce' button to do so.") % (move.raw_material_production_id.product_id.name, move.product_id.name)) + # TODO master: remove me, no longer used def _check_phantom_bom(self, cr, uid, move, context=None): """check if product associated to move has a phantom bom return list of ids of mrp.bom for that product """ @@ -59,24 +60,28 @@ class StockMove(osv.osv): ('company_id', '=', user_company)] return self.pool.get('mrp.bom').search(cr, SUPERUSER_ID, domain, context=context) + def _action_explode(self, cr, uid, move, context=None): """ Explodes pickings. @param move: Stock moves @return: True """ + if context is None: + context = {} bom_obj = self.pool.get('mrp.bom') move_obj = self.pool.get('stock.move') prod_obj = self.pool.get("product.product") proc_obj = self.pool.get("procurement.order") uom_obj = self.pool.get("product.uom") to_explode_again_ids = [] - processed_ids = [] - bis = self._check_phantom_bom(cr, uid, move, context=context) - if bis: - bom_point = bom_obj.browse(cr, SUPERUSER_ID, bis[0], context=context) + property_ids = context.get('property_ids') or [] + bis = bom_obj._bom_find(cr, SUPERUSER_ID, product_id=move.product_id.id, properties=property_ids) + bom_point = bom_obj.browse(cr, SUPERUSER_ID, bis, context=context) + if bis and bom_point.type == 'phantom': + processed_ids = [] factor = uom_obj._compute_qty(cr, SUPERUSER_ID, move.product_uom.id, move.product_uom_qty, bom_point.product_uom.id) / bom_point.product_qty - res = bom_obj._bom_explode(cr, SUPERUSER_ID, bom_point, move.product_id, factor, [], context=context) - + res = bom_obj._bom_explode(cr, SUPERUSER_ID, bom_point, move.product_id, factor, property_ids, context=context) + for line in res[0]: product = prod_obj.browse(cr, uid, line['product_id'], context=context) if product.type != 'service': @@ -115,7 +120,6 @@ class StockMove(osv.osv): else: proc = proc_obj.create(cr, uid, valdef, context=context) proc_obj.run(cr, uid, [proc], context=context) #could be omitted - #check if new moves needs to be exploded if to_explode_again_ids: @@ -134,8 +138,10 @@ class StockMove(osv.osv): #delete the move with original product which is not relevant anymore move_obj.unlink(cr, SUPERUSER_ID, [move.id], context=context) - #return list of newly created move or the move id otherwise, unless there is no move anymore - return processed_ids or (not bis and [move.id]) or [] + #return list of newly created move + return processed_ids + + return [move.id] def action_confirm(self, cr, uid, ids, context=None): move_ids = [] diff --git a/addons/mrp_operations/mrp_operations.py b/addons/mrp_operations/mrp_operations.py index 86a50626558b17a4384fe73ba6c70452250dc0b2..d86c8e6cd63c287267e9f770c75127bc6fa13398 100644 --- a/addons/mrp_operations/mrp_operations.py +++ b/addons/mrp_operations/mrp_operations.py @@ -337,7 +337,7 @@ class mrp_production(osv.osv): for po in self.browse(cr, uid, ids, context=context): direction[po.id] = cmp(po.date_start, vals.get('date_start', False)) result = super(mrp_production, self).write(cr, uid, ids, vals, context=context) - if (vals.get('workcenter_lines', False) or vals.get('date_start', False)) and update: + if (vals.get('workcenter_lines', False) or vals.get('date_start', False) or vals.get('date_planned', False)) and update: self._compute_planned_workcenter(cr, uid, ids, context=context, mini=mini) for d in direction: if direction[d] == 1: diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py index 74ba88065282e6cc2bf3b83938ae131e41dcc3c3..73380dcfda7990b8c82e0bc628297e49b558e76d 100644 --- a/addons/mrp_repair/mrp_repair.py +++ b/addons/mrp_repair/mrp_repair.py @@ -409,6 +409,7 @@ class mrp_repair(osv.osv): 'price_subtotal': fee.product_uom_qty * fee.price_unit }) repair_fee_obj.write(cr, uid, [fee.id], {'invoiced': True, 'invoice_line_id': invoice_fee_id}) + #inv_obj.button_reset_taxes(cr, uid, inv_id, context=context) res[repair.id] = inv_id return res @@ -559,11 +560,16 @@ class mrp_repair_line(osv.osv, ProductChangeMixin): @return: Dictionary of values. """ res = {} - cur_obj = self.pool.get('res.currency') + tax_obj = self.pool.get('account.tax') + # cur_obj = self.pool.get('res.currency') for line in self.browse(cr, uid, ids, context=context): - res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0 - cur = line.repair_id.pricelist_id.currency_id - res[line.id] = cur_obj.round(cr, uid, cur, res[line.id]) + if line.to_invoice: + cur = line.repair_id.pricelist_id.currency_id + taxes = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur.id, line.product_uom_qty, line.product_id, line.repair_id.partner_id) + #res[line.id] = cur_obj.round(cr, uid, cur, taxes['total']) + res[line.id] = taxes['total_included'] + else: + res[line.id] = 0 return res _columns = { @@ -650,11 +656,15 @@ class mrp_repair_fee(osv.osv, ProductChangeMixin): @return: Dictionary of values. """ res = {} + tax_obj = self.pool.get('account.tax') cur_obj = self.pool.get('res.currency') for line in self.browse(cr, uid, ids, context=context): - res[line.id] = line.to_invoice and line.price_unit * line.product_uom_qty or 0 - cur = line.repair_id.pricelist_id.currency_id - res[line.id] = cur_obj.round(cr, uid, cur, res[line.id]) + if line.to_invoice: + cur = line.repair_id.pricelist_id.currency_id + taxes = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur.id, line.product_uom_qty, line.product_id, line.repair_id.partner_id) + res[line.id] = taxes['total_included'] + else: + res[line.id] = 0 return res _columns = { diff --git a/addons/note/note.py b/addons/note/note.py index 2db3173a907c999a7db001f4103cfcb1d50c0eec..fa69b96050e85473f56331bd01a2d7f1cfc4218c 100644 --- a/addons/note/note.py +++ b/addons/note/note.py @@ -168,7 +168,7 @@ class note_note(osv.osv): return result else: - return super(note_note, self).read_group(self, cr, uid, domain, fields, groupby, + return super(note_note, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby,lazy=lazy) diff --git a/addons/payment/models/res_config.py b/addons/payment/models/res_config.py index b64b698c83d7ed86398ba610a9e5e728d258f659..888e0a89fda4ed8b5d7ba027b29e5a104b8c67f2 100644 --- a/addons/payment/models/res_config.py +++ b/addons/payment/models/res_config.py @@ -19,7 +19,7 @@ class AccountPaymentConfig(osv.TransientModel): 'module_payment_buckaroo': fields.boolean( 'Manage Payments Using Buckaroo', help='-It installs the module payment_buckaroo.'), - 'module_payment_authorize': fields.boolean( + 'module_payment_authorize': fields.dummy( 'Manage Payments Using Authorize.Net', help='-It installs the module payment_authorize.'), } diff --git a/addons/payment/views/res_config_view.xml b/addons/payment/views/res_config_view.xml index 8f5e5f31321971ff9f0ec9bee5607d3cf576f04d..e4e6fc96652cf75058dee5f41283251fbae577d9 100644 --- a/addons/payment/views/res_config_view.xml +++ b/addons/payment/views/res_config_view.xml @@ -24,10 +24,6 @@ <field name="module_payment_buckaroo" class="oe_inline"/> <label for="module_payment_buckaroo"/> </div> - <div> - <field name="module_payment_authorize" class="oe_inline"/> - <label for="module_payment_authorize"/> - </div> </xpath> </field> </record> diff --git a/addons/payment_adyen/controllers/main.py b/addons/payment_adyen/controllers/main.py index e6e74eba9a754aca03010940e62f6d799aecfcfa..63b8c9a8fb5a476adbe8e10403ba5441de2873a4 100644 --- a/addons/payment_adyen/controllers/main.py +++ b/addons/payment_adyen/controllers/main.py @@ -20,11 +20,10 @@ class AdyenController(http.Controller): @http.route([ '/payment/adyen/return/', ], type='http', auth='none') - def adyen_return(self, pspReference, **post): - """ Paypal IPN.""" - post["pspReference"] = pspReference + def adyen_return(self, **post): _logger.info('Beginning Adyen form_feedback with post data %s', pprint.pformat(post)) # debug - request.registry['payment.transaction'].form_feedback(request.cr, SUPERUSER_ID, post, 'adyen', context=request.context) + if post.get('authResult') not in ['CANCELLED']: + request.registry['payment.transaction'].form_feedback(request.cr, SUPERUSER_ID, post, 'adyen', context=request.context) return_url = post.pop('return_url', '') if not return_url: custom = json.loads(post.pop('merchantReturnData', '{}')) diff --git a/addons/payment_adyen/models/adyen.py b/addons/payment_adyen/models/adyen.py index 7a99580922280a9d1139b667bdc0210532957e41..6539ebb5ae08f77fe8d5b6252be8f72291b4d965 100644 --- a/addons/payment_adyen/models/adyen.py +++ b/addons/payment_adyen/models/adyen.py @@ -28,7 +28,7 @@ class AcquirerAdyen(osv.Model): - yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple """ return { - 'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % environment, + 'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % 'live' if environment == 'prod' else environment, } def _get_providers(self, cr, uid, context=None): @@ -172,7 +172,7 @@ class TxAdyen(osv.Model): }) return True else: - error = _('Paypal: feedback error') + error = _('Adyen: feedback error') _logger.info(error) tx.write({ 'state': 'error', diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml index 9670da9f7a515b68c664d56c31c1ee38e33f0c6e..5af35bf7092f6ea00ae3bf471c9541ef6d926100 100644 --- a/addons/point_of_sale/point_of_sale_view.xml +++ b/addons/point_of_sale/point_of_sale_view.xml @@ -904,7 +904,7 @@ <field name="name" /> <filter string="Open" domain="[('state', '=', 'opened')]" /> <separator/> - <filter string="Today" domain="[('start_at', '>=', datetime.datetime.now().replace(hour=0, minute=0, second=0))]" /> + <filter string="Today" domain="[('start_at', '>=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]" /> <field name="config_id" /> <field name="user_id" /> <group expand="0" string="Group By"> diff --git a/addons/point_of_sale/report/pos_details.py b/addons/point_of_sale/report/pos_details.py index 8ff15c9036cb2e1c5c7ef218bf8a3426d5cb0fe1..4d9ce4372fa732c196c665af3063c6115a17a0b0 100644 --- a/addons/point_of_sale/report/pos_details.py +++ b/addons/point_of_sale/report/pos_details.py @@ -129,13 +129,7 @@ class pos_details(report_sxw.rml_parse): return {} def _total_of_the_day(self, objects): - if self.total: - if self.total == self.total_invoiced: - return self.total - else: - return ((self.total or 0.00) - (self.total_invoiced or 0.00)) - else: - return False + return self.total or 0.00 def _sum_invoice(self, objects): return reduce(lambda acc, obj: diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index cfd842f86dae4693442e7fc1d85b6cb2ef07c08f..9a8df01feae74c00dc84c7f8a87cc62a868d7d1a 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -171,7 +171,7 @@ var PosDB = core.Class.extend({ for (var i = 0; i < packagings.length; i++) { str += '|' + packagings[i].barcode; } - str = product.id + ':' + str.replace(':','') + '\n'; + str = product.id + ':' + str.replace(/:/g,'') + '\n'; return str; }, add_products: function(products){ diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 511e0934970068aee6fbbd5dcc09e0dfc6f347d5..a8560479c521f171b5548e29d5ce2daa9300a83b 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -153,7 +153,7 @@ exports.PosModel = Backbone.Model.extend({ loaded: function(self,users){ self.user = users[0]; }, },{ model: 'res.company', - fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id'], + fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id', 'tax_calculation_rounding_method'], ids: function(self){ return [self.user.company_id[0]]; }, loaded: function(self,companies){ self.company = companies[0]; }, },{ @@ -1284,6 +1284,9 @@ exports.Orderline = Backbone.Model.extend({ var total_included = total_excluded; var base = total_excluded; var list_taxes = []; + if (this.pos.company.tax_calculation_rounding_method == "round_globally"){ + currency_rounding = currency_rounding * 0.00001; + } _(taxes).each(function(tax) { if (tax.amount_type === 'group'){ var ret = self.compute_all(tax.children_tax_ids, price_unit, quantity, currency_rounding); @@ -1811,34 +1814,34 @@ exports.Order = Backbone.Model.extend({ }, /* ---- Payment Status --- */ get_subtotal : function(){ - return this.orderlines.reduce((function(sum, orderLine){ + return round_pr(this.orderlines.reduce((function(sum, orderLine){ return sum + orderLine.get_display_price(); - }), 0); + }), 0), this.pos.currency.rounding); }, get_total_with_tax: function() { - return this.orderlines.reduce((function(sum, orderLine) { + return round_pr(this.orderlines.reduce((function(sum, orderLine) { return sum + orderLine.get_price_with_tax(); - }), 0); + }), 0), this.pos.currency.rounding); }, get_total_without_tax: function() { - return this.orderlines.reduce((function(sum, orderLine) { + return round_pr(this.orderlines.reduce((function(sum, orderLine) { return sum + orderLine.get_price_without_tax(); - }), 0); + }), 0), this.pos.currency.rounding); }, get_total_discount: function() { - return this.orderlines.reduce((function(sum, orderLine) { + return round_pr(this.orderlines.reduce((function(sum, orderLine) { return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); - }), 0); + }), 0), this.pos.currency.rounding); }, get_total_tax: function() { - return this.orderlines.reduce((function(sum, orderLine) { + return round_pr(this.orderlines.reduce((function(sum, orderLine) { return sum + orderLine.get_tax(); - }), 0); + }), 0), this.pos.currency.rounding); }, get_total_paid: function() { - return this.paymentlines.reduce((function(sum, paymentLine) { + return round_pr(this.paymentlines.reduce((function(sum, paymentLine) { return sum + paymentLine.get_amount(); - }), 0); + }), 0), this.pos.currency.rounding); }, get_tax_details: function(){ var details = {}; diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index b951ed30d8cce654c7442aeacd2d89c3019235a9..5ab69194a2597c8c96eefbed450cb88693ea1c63 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1480,6 +1480,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ this.inputbuffer = ""; this.firstinput = true; + this.decimal_point = instance.web._t.database.parameters.decimal_point; // This is a keydown handler that prevents backspace from // doing a back navigation @@ -1545,7 +1546,14 @@ var PaymentScreenWidget = ScreenWidget.extend({ this.inputbuffer = newbuf; var order = this.pos.get_order(); if (order.selected_paymentline) { - order.selected_paymentline.set_amount(parseFloat(this.inputbuffer)); + var amount; + try{ + amount = instance.web.parse_value(this.inputbuffer, {type: "float"}); + } + catch(e){ + amount = 0; + } + order.selected_paymentline.set_amount(amount); this.order_changes(); this.render_paymentlines(); this.$('.paymentline.selected .edit').text(this.inputbuffer); diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js index 7c885989f2ba5cb73db0299df9b01ae93835a2d6..f051695df96f7b53373f34841795355cd2cae1f2 100644 --- a/addons/point_of_sale/static/src/js/widget_base.js +++ b/addons/point_of_sale/static/src/js/widget_base.js @@ -50,6 +50,7 @@ var PosBaseWidget = Widget.extend({ if (typeof amount === 'number') { amount = round_di(amount,decimals).toFixed(decimals); + amount = openerp.instances[this.session.name].web.format_value(parseFloat(amount), { type : 'float' }); } return amount; diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index da039276c3e731bd36831e47e0beb19353f7dd5c..79eacc551d49cee4840dc5a72834a8e49d03fd61 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -323,7 +323,7 @@ <select class='detail client-address-country' name='country_id'> <option value=''>None</option> <t t-foreach='widget.pos.countries' t-as='country'> - <option t-att-value='country.id' t-att-selected="partner_country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined"> + <option t-att-value='country.id' t-att-selected="partner.country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined"> <t t-esc='country.name'/> </option> </t> @@ -1189,7 +1189,10 @@ <div class='paymentline-name'> <t t-esc="line.name"/> </div> - <input class='paymentline-input' type="number" step="0.01" t-att-value="line.get_amount_str()" /> + <input class='paymentline-input' + t-att-type="widget.decimal_point === '.' ? 'number' : 'text'" + t-attf-pattern="[0-9]+([\\#{widget.decimal_point || '.' }][0-9]+)?" + step="0.01" t-att-value="line.get_amount_str()" /> <span class='paymentline-delete'> <img src="/point_of_sale/static/src/img/search_reset.gif" /> </span> diff --git a/addons/point_of_sale/wizard/pos_details.py b/addons/point_of_sale/wizard/pos_details.py index 7b1dde7e0d65bb3bb86e57610b48f1c2953ac2f9..db9dde51a4d08a871ef7033f3ef0f41f29c4fd7f 100644 --- a/addons/point_of_sale/wizard/pos_details.py +++ b/addons/point_of_sale/wizard/pos_details.py @@ -33,8 +33,8 @@ class pos_details(osv.osv_memory): 'user_ids': fields.many2many('res.users', 'pos_details_report_user_rel', 'user_id', 'wizard_id', 'Salespeople'), } _defaults = { - 'date_start': lambda *a: time.strftime('%Y-%m-%d'), - 'date_end': lambda *a: time.strftime('%Y-%m-%d'), + 'date_start': fields.date.context_today, + 'date_end': fields.date.context_today, } def print_report(self, cr, uid, ids, context=None): diff --git a/addons/portal/wizard/share_wizard.py b/addons/portal/wizard/share_wizard.py index 7fafff9a862eccd07a94d90ffe528234432c0fb4..188ee64983de577497c297bbff3e9d340adb39ce 100644 --- a/addons/portal/wizard/share_wizard.py +++ b/addons/portal/wizard/share_wizard.py @@ -19,12 +19,13 @@ # ############################################################################## +from openerp import SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools.translate import _ import logging _logger = logging.getLogger(__name__) -UID_ROOT = 1 +UID_ROOT = SUPERUSER_ID SHARED_DOCS_MENU = "Documents" SHARED_DOCS_CHILD_MENU = "Shared Documents" @@ -56,9 +57,18 @@ class share_wizard_portal(osv.TransientModel): return super(share_wizard_portal, self)._check_preconditions(cr, uid, wizard_data, context=context) def _create_or_get_submenu_named(self, cr, uid, parent_menu_id, menu_name, context=None): - if not parent_menu_id: - return + if context is None: + context = {} Menus = self.pool.get('ir.ui.menu') + if not parent_menu_id and context.get('group_id'): + cxt = dict(context) + cxt['ir.ui.menu.full_list'] = True + parent_menu_ids = Menus.search(cr, SUPERUSER_ID, + [('groups_id', 'in', [context.get('group_id')]), ('parent_id', '=', False)], limit=1, context=cxt) + parent_menu_id = parent_menu_ids and parent_menu_ids[0] or False + if not parent_menu_id: + return False + parent_menu = Menus.browse(cr, uid, parent_menu_id) # No context menu_id = None max_seq = 10 @@ -79,9 +89,12 @@ class share_wizard_portal(osv.TransientModel): def _sharing_root_menu_id(self, cr, uid, portal, context=None): """Create or retrieve root ID of sharing menu in portal menu - :param portal: browse_record of portal, constructed with a context WITHOUT language + :param portal: browse_record of shared group, constructed with a context WITHOUT language """ - parent_menu_id = self._create_or_get_submenu_named(cr, uid, portal.parent_menu_id.id, SHARED_DOCS_MENU, context=context) + if context is None: + context = {} + ctx = dict(context, group_id=portal.id) + parent_menu_id = self._create_or_get_submenu_named(cr, uid, False, SHARED_DOCS_MENU, context=ctx) if parent_menu_id: child_menu_id = self._create_or_get_submenu_named(cr, uid, parent_menu_id, SHARED_DOCS_CHILD_MENU, context=context) return child_menu_id @@ -90,7 +103,7 @@ class share_wizard_portal(osv.TransientModel): """Create sharing menus in portal menu according to share wizard options. :param wizard_data: browse_record of share.wizard - :param portal: browse_record of portal, constructed with a context WITHOUT language + :param portal: browse_record of shared group, constructed with a context WITHOUT language """ root_menu_id = self._sharing_root_menu_id(cr, uid, portal, context=context) if not root_menu_id: @@ -126,13 +139,16 @@ class share_wizard_portal(osv.TransientModel): # setup the menu for portal groups for group in wizard_data.group_ids: if group.id in all_portal_group_ids: - self._create_shared_data_menu(cr, uid, wizard_data, group.id, context=context) + self._create_shared_data_menu(cr, uid, wizard_data, group, context=context) for user in group.users: new_line = {'user_id': user.id, 'newly_created': False} wizard_data.write({'result_line_ids': [(0,0,new_line)]}) + selected_group_ids = [x.id for x in wizard_data.group_ids] + res_groups.write(cr, SUPERUSER_ID, selected_group_ids, {'implied_ids': [(4, super_result[0])]}) + elif wizard_data.user_ids: # must take care of existing users, by adding them to the new group, which is super_result[0], # and adding the shortcut diff --git a/addons/pos_restaurant/static/src/js/splitbill.js b/addons/pos_restaurant/static/src/js/splitbill.js index 9dd7e7449e2b7206137aff3c6b4c7c39cabf4277..745c8282c20277f2ce146cbee2d503f941d47cb3 100644 --- a/addons/pos_restaurant/static/src/js/splitbill.js +++ b/addons/pos_restaurant/static/src/js/splitbill.js @@ -52,7 +52,7 @@ var SplitbillScreenWidget = screens.ScreenWidget.extend({ } }else{ if( split.quantity < line.get_quantity()){ - split.quantity += line.get_unit().rounding; + split.quantity += line.get_unit().is_unit ? 1 : line.get_unit().rounding; if(split.quantity > line.get_quantity()){ split.quantity = line.get_quantity(); } diff --git a/addons/product/partner_view.xml b/addons/product/partner_view.xml index 00f957aaa4b7fb3d46d3b4ca5782b05863026d53..a713943fd1b0749de6a6e35d9ec65993d1e1a8a8 100644 --- a/addons/product/partner_view.xml +++ b/addons/product/partner_view.xml @@ -5,6 +5,7 @@ <field name="name">res.partner.product.property.form.inherit</field> <field name="model">res.partner</field> <field name="inherit_id" ref="base.view_partner_form"/> + <field name="groups_id" eval="[(4, ref('product.group_sale_pricelist')), (4, ref('product.group_purchase_pricelist'))]"/> <field name="arch" type="xml"> <group name="sale"> <field name="property_product_pricelist" groups="product.group_sale_pricelist" attrs="{'invisible': [('is_company','=',False),('parent_id','!=',False)]}"/> diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py index 3b4bf2451d94bb71f7bdb0e27109b4c6771cfabf..fee4caee12a930ec1e072f4490e431a681733e72 100644 --- a/addons/product/pricelist.py +++ b/addons/product/pricelist.py @@ -19,6 +19,7 @@ # ############################################################################## +from itertools import chain import time from openerp import tools @@ -224,7 +225,9 @@ class product_pricelist(osv.osv): is_product_template = products[0]._name == "product.template" if is_product_template: prod_tmpl_ids = [tmpl.id for tmpl in products] - prod_ids = [product.id for product in tmpl.product_variant_ids for tmpl in products] + # all variants of all products + prod_ids = [p.id for p in + list(chain.from_iterable([t.product_variant_ids for t in products]))] else: prod_ids = [product.id for product in products] prod_tmpl_ids = [product.product_tmpl_id.id for product in products] @@ -274,7 +277,9 @@ class product_pricelist(osv.osv): if is_product_template: if rule.product_tmpl_id and product.id != rule.product_tmpl_id.id: continue - if rule.product_id: + if rule.product_id and \ + (product.product_variant_count > 1 or product.product_variant_ids[0].id != rule.product_id.id): + # product rule acceptable on template if has only one variant continue else: if rule.product_tmpl_id and product.product_tmpl_id.id != rule.product_tmpl_id.id: diff --git a/addons/product/product.py b/addons/product/product.py index 4e11f550dc02af004fc186faae8bcec70d996fd9..4cea70d6d5afa7e7f95ddd1304c911910b659607 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -655,6 +655,9 @@ class product_template(osv.osv): res = False return res + def onchange_type(self, cr, uid, ids, type): + return {} + def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id): if uom_id: return {'value': {'uom_po_id': uom_id}} @@ -1013,6 +1016,9 @@ class product_product(osv.osv): self.pool.get('product.template').unlink(cr, uid, unlink_product_tmpl_ids, context=context) return res + def onchange_type(self, cr, uid, ids, type): + return {} + def onchange_uom(self, cursor, user, ids, uom_id, uom_po_id): if uom_id and uom_po_id: uom_obj=self.pool.get('product.uom') diff --git a/addons/product/product_view.xml b/addons/product/product_view.xml index 2d3150bfe621bce2da0bca18016a3ba559505978..c9ce526998380462a87351e852a275acf16318ca 100644 --- a/addons/product/product_view.xml +++ b/addons/product/product_view.xml @@ -72,7 +72,7 @@ <page string="Information"> <group colspan="4"> <group> - <field name="type"/> + <field name="type" on_change="onchange_type(type)"/> <field name="uom_id" on_change="onchange_uom(uom_id,uom_po_id)" groups="product.group_uom"/> <field name="list_price"/> </group> diff --git a/addons/product_visible_discount/product_visible_discount.py b/addons/product_visible_discount/product_visible_discount.py index f9dfb3b409c9ee473d1652c8175245b3705f1545..cced048ab112808ddef8744fbe1badbe03afaa3c 100644 --- a/addons/product_visible_discount/product_visible_discount.py +++ b/addons/product_visible_discount/product_visible_discount.py @@ -42,26 +42,34 @@ class sale_order_line(osv.osv): lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position_id=False, flag=False, context=None): - def get_real_price(res_dict, product_id, qty, uom, pricelist): + def get_real_price_curency(res_dict, product_id, qty, uom, pricelist): """Retrieve the price before applying the pricelist""" item_obj = self.pool.get('product.pricelist.item') price_type_obj = self.pool.get('product.price.type') product_obj = self.pool.get('product.product') field_name = 'list_price' rule_id = res_dict.get(pricelist) and res_dict[pricelist][1] or False + currency_id = None if rule_id: item_base = item_obj.read(cr, uid, [rule_id], ['base'])[0]['base'] if item_base > 0: - field_name = price_type_obj.browse(cr, uid, item_base).field + price_type = price_type_obj.browse(cr, uid, item_base) + field_name = price_type.field + currency_id = price_type.currency_id product = product_obj.browse(cr, uid, product_id, context) product_read = product_obj.read(cr, uid, [product_id], [field_name], context=context)[0] + if not currency_id: + currency_id = product.company_id.currency_id.id factor = 1.0 if uom and uom != product.uom_id.id: # the unit price is in a different uom factor = self.pool['product.uom']._compute_qty(cr, uid, uom, 1.0, product.uom_id.id) - return product_read[field_name] * factor + return product_read[field_name] * factor, currency_id + + def get_real_price(res_dict, product_id, qty, uom, pricelist): + return get_real_price_curency(res_dict, product_id, qty, uom, pricelist)[0] res=super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty, @@ -85,14 +93,14 @@ class sale_order_line(osv.osv): so_pricelist = pricelist_obj.browse(cr, uid, pricelist, context=context) - new_list_price = get_real_price(list_price, product.id, qty, uom, pricelist) + new_list_price, currency_id = get_real_price_curency(list_price, product.id, qty, uom, pricelist) if so_pricelist.visible_discount and list_price[pricelist][0] != 0 and new_list_price != 0: if product.company_id and so_pricelist.currency_id.id != product.company_id.currency_id.id: # new_list_price is in company's currency while price in pricelist currency ctx = context.copy() ctx['date'] = date_order new_list_price = self.pool['res.currency'].compute(cr, uid, - product.company_id.currency_id.id, so_pricelist.currency_id.id, + currency_id.id, so_pricelist.currency_id.id, new_list_price, context=ctx) discount = (new_list_price - price) / new_list_price * 100 if discount > 0: diff --git a/addons/project/project.py b/addons/project/project.py index 7abab996c56ffeb48d38725d2db9d4aae414f7a2..d57e91d007d2ac6529b894d74ca6a52688f468a6 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -900,13 +900,20 @@ class task(osv.osv): context = context or {} result = "" ident = ' '*ident + company = self.pool["res.users"].browse(cr, uid, uid, context=context).company_id + duration_uom = { + 'day(s)': 'd', 'days': 'd', 'day': 'd', 'd': 'd', + 'month(s)': 'm', 'months': 'm', 'month': 'month', 'm': 'm', + 'week(s)': 'w', 'weeks': 'w', 'week': 'w', 'w': 'w', + 'hour(s)': 'H', 'hours': 'H', 'hour': 'H', 'h': 'H', + }.get(company.project_time_mode_id.name.lower(), "hour(s)") for task in tasks: if task.stage_id and task.stage_id.fold: continue result += ''' %sdef Task_%s(): -%s todo = \"%.2fH\" -%s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident, task._get_total_hours()) +%s todo = \"%.2f%s\" +%s effort = \"%.2f%s\"''' % (ident, task.id, ident, task.remaining_hours, duration_uom, ident, task._get_total_hours(), duration_uom) start = [] for t2 in task.parent_ids: start.append("up.Task_%s.end" % (t2.id,)) diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index a5be3b422e774ef1809851762b9995890d9ac961..7ad533c92fc09f76618526bbe747aa214cb61bcc 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -640,7 +640,7 @@ <field name="arch" type="xml"> <xpath expr="//group[@name='account_grp']" position="after"> <group name="project_grp" string="Projects"> - <field name="project_time_mode_id" domain="[('category_id','=','Working Time')]"/> + <field name="project_time_mode_id" domain="[('category_id','=', %(product.uom_categ_wtime)d)]"/> </group> </xpath> </field> diff --git a/addons/project/static/src/js/web_planner_project.js b/addons/project/static/src/js/web_planner_project.js index 00947447c9ad991b83dd18dd38a0df44509a63f9..28e5a1f9e4f1d4b10b2aad848a120ccab04b5f18 100644 --- a/addons/project/static/src/js/web_planner_project.js +++ b/addons/project/static/src/js/web_planner_project.js @@ -1,7 +1,7 @@ odoo.define('planner_project.planner', function (require) { "use strict"; -var planner = require('web.planner'); +var planner = require('web.planner.common'); var core = require('web.core'); var _t = core._t; diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 1b47a124cdafa9c3b8b3877e7bcf4e65a2c48f19..92d11c9029dc804ed612d96478438647c17e019a 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -219,6 +219,7 @@ class project_issue(osv.Model): _defaults = { 'active': 1, + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), 'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c), 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c), 'priority': '0', diff --git a/addons/purchase/partner_view.xml b/addons/purchase/partner_view.xml index 5e9cce58df6a4213f3b175ccb073c4915e55ebbf..060a7e3a2facdd8b625f4d8d1bdbef21a38b0f84 100644 --- a/addons/purchase/partner_view.xml +++ b/addons/purchase/partner_view.xml @@ -6,9 +6,10 @@ <field name="model">res.partner</field> <field name="inherit_id" ref="base.view_partner_form"/> <field name="priority">36</field> + <field name="groups_id" eval="[(4, ref('product.group_purchase_pricelist'))]"/> <field name="arch" type="xml"> <group name="purchase" position="inside"> - <field name="property_product_pricelist_purchase" groups="product.group_purchase_pricelist"/> + <field name="property_product_pricelist_purchase"/> </group> </field> </record> diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index fec5f60fbdedbd3b23fa5e6a8e2acaec0c76b577..abb0209b14eb0522096b374c100495b8643ba978 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -402,11 +402,16 @@ class purchase_order(osv.osv): 'fiscal_position_id': False, 'payment_term_id': False, }} + + company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) + if not company_id: + raise osv.except_osv(_('Error!'), _('There is no default company for the current user!')) + fp = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, context=context) supplier_address = partner.address_get(cr, uid, [partner_id], ['default'], context=context) supplier = partner.browse(cr, uid, partner_id, context=context) return {'value': { 'pricelist_id': supplier.property_product_pricelist_purchase.id, - 'fiscal_position_id': supplier.property_account_position and supplier.property_account_position.id or False, + 'fiscal_position_id': fp or supplier.property_account_position and supplier.property_account_position.id or False, 'payment_term_id': supplier.property_supplier_payment_term.id or False, }} @@ -1027,7 +1032,7 @@ class purchase_order_line(osv.osv): def unlink(self, cr, uid, ids, context=None): for line in self.browse(cr, uid, ids, context=context): - if line.state not in ['draft', 'cancel']: + if line.order_id.state in ['approved', 'done'] and line.state not in ['draft', 'cancel']: raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,)) procurement_obj = self.pool.get('procurement.order') procurement_ids_to_except = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context) @@ -1200,18 +1205,19 @@ class procurement_order(osv.osv): uom_obj = self.pool.get("product.uom") for procurement in self.browse(cr, uid, ids, context=context): if procurement.rule_id.action == 'buy' and procurement.purchase_line_id: - uom = procurement.purchase_line_id.product_uom - product_qty = uom_obj._compute_qty_obj(cr, uid, procurement.product_uom, procurement.product_qty, uom, context=context) if procurement.purchase_line_id.state not in ('draft', 'cancel'): raise UserError( _('Can not cancel this procurement like this as the related purchase order has been confirmed already. Please cancel the purchase order first. ')) - if float_compare(procurement.purchase_line_id.product_qty, product_qty, 0, precision_rounding=uom.rounding) > 0: - purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': procurement.purchase_line_id.product_qty - product_qty}, context=context) - else: + + new_qty, new_price = self._calc_new_qty_price(cr, uid, procurement, cancel=True, context=context) + if new_qty != procurement.purchase_line_id.product_qty: + purchase_line_obj.write(cr, uid, [procurement.purchase_line_id.id], {'product_qty': new_qty, 'price_unit': new_price}, context=context) + if float_compare(new_qty, 0.0, precision_rounding=procurement.product_uom.rounding) != 1: if procurement.purchase_line_id.id not in lines_to_cancel: lines_to_cancel += [procurement.purchase_line_id.id] if lines_to_cancel: purchase_line_obj.action_cancel(cr, uid, lines_to_cancel, context=context) + purchase_line_obj.unlink(cr, uid, lines_to_cancel, context=context) return super(procurement_order, self).propagate_cancels(cr, uid, ids, context=context) def _run(self, cr, uid, procurement, context=None): @@ -1375,6 +1381,41 @@ class procurement_order(osv.osv): res[procurement.id] = values return res + def _calc_new_qty_price(self, cr, uid, procurement, po_line=None, cancel=False, context=None): + if not po_line: + po_line = procurement.purchase_line_id + + uom_obj = self.pool.get('product.uom') + qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, + procurement.product_id.uom_po_id.id) + if cancel: + qty = -qty + + # Make sure we use the minimum quantity of the partner corresponding to the PO + if po_line.product_id.seller_id.id == po_line.order_id.partner_id.id: + supplierinfo_min_qty = po_line.product_id.seller_qty + else: + supplierinfo_obj = self.pool.get('product.supplierinfo') + supplierinfo_ids = supplierinfo_obj.search(cr, uid, [('name', '=', po_line.order_id.partner_id.id), ('product_tmpl_id', '=', po_line.product_id.product_tmpl_id.id)]) + supplierinfo_min_qty = supplierinfo_obj.browse(cr, uid, supplierinfo_ids).min_qty + + if supplierinfo_min_qty == 0.0: + qty += po_line.product_qty + else: + # Recompute quantity by adding existing running procurements. + for proc in po_line.procurement_ids: + qty += uom_obj._compute_qty(cr, uid, proc.product_uom.id, proc.product_qty, + proc.product_id.uom_po_id.id) if proc.state == 'running' else 0.0 + qty = max(qty, supplierinfo_min_qty) if qty > 0.0 else 0.0 + + price = po_line.price_unit + if qty != po_line.product_qty: + pricelist_obj = self.pool.get('product.pricelist') + pricelist_id = po_line.order_id.partner_id.property_product_pricelist_purchase.id + price = pricelist_obj.price_get(cr, uid, [pricelist_id], procurement.product_id.id, qty, po_line.order_id.partner_id.id, {'uom': procurement.product_uom.id})[pricelist_id] + + return qty, price + def _get_grouping_dicts(self, cr, uid, ids, context=None): """ It will group the procurements according to the pos they should go into. That way, lines going to the same @@ -1459,8 +1500,11 @@ class procurement_order(osv.osv): for proc in procurements: if po_prod_dict.get(proc.product_id.id): po_line = po_prod_dict[proc.product_id.id] - uom_id = po_line.product_uom #Convert to UoM of existing line + # FIXME: compute quantity using `_calc_new_qty_price` method. + # new_qty, new_price = self._calc_new_qty_price(cr, uid, proc, po_line=po_line, context=context) + uom_id = po_line.product_uom # Convert to UoM of existing line qty = uom_obj._compute_qty_obj(cr, uid, proc.product_uom, proc.product_qty, uom_id) + if lines_to_update.get(po_line): lines_to_update[po_line] += [(proc.id, qty)] else: @@ -1645,15 +1689,21 @@ class account_invoice(osv.Model): else: user_id = uid po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context) - for order in purchase_order_obj.browse(cr, uid, po_ids, context=context): + for order in purchase_order_obj.browse(cr, user_id, po_ids, context=context): purchase_order_obj.message_post(cr, user_id, order.id, body=_("Invoice received"), context=context) invoiced = [] + shipped = True + # for invoice method manual or order, don't care about shipping state + # for invoices based on incoming shippment, beware of partial deliveries + if (order.invoice_method == 'picking' and + not all(picking.invoice_state in ['invoiced'] for picking in order.picking_ids)): + shipped = False for po_line in order.order_line: - if any(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines): + if all(line.invoice_id.state not in ['draft', 'cancel'] for line in po_line.invoice_lines): invoiced.append(po_line.id) - if invoiced: - self.pool['purchase.order.line'].write(cr, uid, invoiced, {'invoiced': True}) - workflow.trg_write(uid, 'purchase.order', order.id, cr) + if invoiced and shipped: + self.pool['purchase.order.line'].write(cr, user_id, invoiced, {'invoiced': True}) + workflow.trg_write(user_id, 'purchase.order', order.id, cr) return res def confirm_paid(self, cr, uid, ids, context=None): diff --git a/addons/purchase/report/purchase_report.py b/addons/purchase/report/purchase_report.py index 096f0d86e7b6633d0d6f8b6eb6b6776807ebdbfb..90b3d28a66a9288c83f79527c62a5e4999bb0813 100644 --- a/addons/purchase/report/purchase_report.py +++ b/addons/purchase/report/purchase_report.py @@ -79,7 +79,7 @@ class purchase_report(osv.osv): s.dest_address_id, s.pricelist_id, s.validator, - s.picking_type_id as picking_type_id, + spt.warehouse_id as picking_type_id, s.partner_id as partner_id, s.create_uid as user_id, s.company_id as company_id, @@ -108,6 +108,7 @@ class purchase_report(osv.osv): LEFT JOIN ir_property ip ON (ip.name='standard_price' AND ip.res_id=CONCAT('product.template,',t.id) AND ip.company_id=s.company_id) left join product_uom u on (u.id=l.product_uom) left join product_uom u2 on (u2.id=t.uom_id) + left join stock_picking_type spt on (spt.id=s.picking_type_id) left join account_analytic_account analytic_account on (l.account_analytic_id = analytic_account.id) group by s.company_id, @@ -129,7 +130,7 @@ class purchase_report(osv.osv): t.categ_id, s.date_order, s.state, - s.picking_type_id, + spt.warehouse_id, u.uom_type, u.category_id, t.uom_id, diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index 16b0fd70d8723c84b0338ae061fc8f7874fea032..dac3203074f201ea8532eb4bb80d67df47e89f9d 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -157,6 +157,10 @@ class stock_move(osv.osv): return self.write(cr, uid, [move.id], {'price_unit': price}, context=context) super(stock_move, self).attribute_price(cr, uid, move, context=context) + def _get_taxes(self, cr, uid, move, context=None): + if move.origin_returned_move_id.purchase_line_id.taxes_id: + return [tax.id for tax in move.origin_returned_move_id.purchase_line_id.taxes_id] + return super(stock_move, self)._get_taxes(cr, uid, move, context=context) class stock_picking(osv.osv): _inherit = 'stock.picking' diff --git a/addons/report/models/report.py b/addons/report/models/report.py index d39bbd17f72d9667ef55b71a166c88ded4366875..555d6ae7a999a008b6588a85926b985743352393 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -22,7 +22,7 @@ from openerp import api from openerp import SUPERUSER_ID from openerp.exceptions import AccessError -from openerp.osv import osv +from openerp.osv import osv, fields from openerp.tools import config from openerp.tools.misc import find_in_path from openerp.tools.translate import _ @@ -140,6 +140,7 @@ class Report(osv.Model): context = dict(context, translatable=context.get('lang') != request.website.default_lang_code) values.update( time=time, + context_timestamp=lambda t: fields.datetime.context_timestamp(cr, uid, t, context), translate_doc=translate_doc, editable=True, user=user, diff --git a/addons/report/static/src/css/reset.min.css b/addons/report/static/src/css/reset.min.css index cc7842e37ef3b33d0cacc109f096de32301a5f11..ee21ad74e7d74757350af54bd4917686e85adaee 100644 --- a/addons/report/static/src/css/reset.min.css +++ b/addons/report/static/src/css/reset.min.css @@ -1,2 +1,106 @@ -/* reset5 2011 opensource.736cs.com MIT - https://code.google.com/p/reset5 */ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,audio,canvas,details,figcaption,figure,footer,header,hgroup,mark,menu,meter,nav,output,progress,section,summary,time,video{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;}body{line-height:1;}article,aside,dialog,figure,footer,header,hgroup,nav,section,blockquote{display:block;}nav ul{list-style:none;}ol{list-style:decimal;}ul{list-style:disc;}ul ul{list-style:circle;}blockquote,q{quotes:none;}blockquote:before,blockquote:after,q:before,q:after{content:none;}ins{text-decoration:underline;}del{text-decoration:line-through;}mark{background:none;}abbr[title],dfn[title]{border-bottom:1px dotted #000;cursor:help;}table{border-collapse:collapse;border-spacing:0;}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;}input[type=submit],input[type=button],button{margin:0!important;padding:0!important;}input,select,a img{vertical-align:middle;} +/* + +HTML5 CSS Reset +Based on Eric Meyer's CSS Reset +and html5doctor.com HTML5 Reset + +Copyright (c) 2011 736 Computing Services Limited +Released under the MIT license. http://opensource.736cs.com/licenses/mit + +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, i, center, dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, audio, canvas, details, figcaption, +figure, footer, header, hgroup, mark, menu, meter, nav, +output, progress, section, summary, time, video { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; +} + +body { + line-height: 1; +} + +article, aside, dialog, figure, footer, header, +hgroup, nav, section, blockquote { + display: block; +} + +nav ul { + list-style: none; +} + +ol { + list-style: decimal; +} + +ul { + list-style: disc; +} + +ul ul { + list-style: circle; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +ins { + text-decoration: underline; +} + +del { + text-decoration: line-through; +} + +mark { + background: none; +} + +abbr[title], dfn[title] { + border-bottom:1px dotted #000; + cursor:help; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +input[type="submit"], input[type="button"], button { + padding: 0 !important; + margin: 0 !important; +} + +input, select, a img { + vertical-align: middle; +} diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml index 9e1924e8a3fc34b1d97c63a7db78f3fce6913081..e61b4d796dbe35d8be33ea13ca2128928b75e012 100644 --- a/addons/report/views/layouts.xml +++ b/addons/report/views/layouts.xml @@ -136,8 +136,7 @@ <div class="header"> <div class="row"> <div class="col-xs-3"> - <span t-esc="time.strftime('%Y-%m-%d')"/> - <span t-esc="time.strftime('%H:%M')"/> + <span t-esc="context_timestamp(datetime.datetime.now()).strftime('%Y-%m-%d %H:%M')"/> </div> <div class="col-xs-2 col-xs-offset-2 text-center"> <span t-esc="company.name"/> @@ -166,7 +165,22 @@ <link href="/base/static/src/css/description.css" rel="stylesheet"/> <style type='text/css'><t t-raw="css"/></style> <t t-if="subst is True"> - <script src='/report/static/src/js/subst.js'></script> + <script> + function subst() { + var vars = {}; + var x = document.location.search.substring(1).split('&'); + for (var i in x) { + var z = x[i].split('=', 2); + vars[z[0]] = unescape(z[1]); + } + var x=['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection']; + for (var i in x) { + var y = document.getElementsByClassName(x[i]); + for (var j=0; j<y.length; ++j) + y[j].textContent = vars[x[i]]; + } + } + </script> </t> </head> <body class="container" onload="subst()"> diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index dbcb1f1415c74a247a08c5c1313a148ce929723c..f67ea3602664f06849fcc67bbc3b10d97390866f 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -87,6 +87,7 @@ The Dashboard for the Sales Manager will include 'test/manual_order_policy.yml', 'test/cancel_order.yml', 'test/delete_order.yml', + 'test/canceled_lines_order.yml', ], 'css': ['static/src/css/sale.css'], 'installable': True, diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index a06c58b7be9c5e32df32a53a4281f302d752e267..5892febd8523c31a8e4430f8a9bc7bd849593675 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -77,7 +77,7 @@ class sale_report(osv.osv): s.user_id as user_id, s.company_id as company_id, extract(epoch from avg(date_trunc('day',s.date_confirm)-date_trunc('day',s.create_date)))/(24*60*60)::decimal(16,2) as delay, - s.state, + l.state, t.categ_id as categ_id, s.pricelist_id as pricelist_id, s.project_id as analytic_account_id, @@ -113,7 +113,7 @@ class sale_report(osv.osv): s.partner_id, s.user_id, s.company_id, - s.state, + l.state, s.pricelist_id, s.project_id, s.team_id, diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 482a7cfe5ded63c990f2ad2c904d7bce18b8d6a2..99d004738618aebe83c5d2b155420865949d2c42 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -111,26 +111,6 @@ class sale_order(osv.osv): raise UserError(_('There is no default company for the current user!')) return company_id - def _get_default_team_id(self, cr, uid, context=None): - """ Gives default team by checking if present in the context """ - team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False - return team_id - - def _resolve_team_id_from_context(self, cr, uid, context=None): - """ Returns ID of team based on the value of 'team_id' - context key, or None if it cannot be resolved to a single - Sales Team. - """ - if context is None: - context = {} - if type(context.get('default_team_id')) in (int, long): - return context.get('default_team_id') - if isinstance(context.get('default_team_id'), basestring): - team_ids = self.pool.get('crm.team').name_search(cr, uid, name=context['default_team_id'], context=context) - if len(team_ids) == 1: - return int(team_ids[0][0]) - return None - def _get_invoiced(self, cr, uid, ids, field_name, arg, context=None): res = {} for order in self.browse(cr, uid, ids, context=context): @@ -220,7 +200,7 @@ class sale_order(osv.osv): 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'], 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'], 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note, - 'team_id': lambda s, cr, uid, c: s._get_default_team_id(cr, uid, c), + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), } _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'), @@ -315,6 +295,8 @@ class sale_order(osv.osv): val.update(delivery_onchange['value']) if pricelist: val['pricelist_id'] = pricelist + if not self.pool['crm.team']._get_default_team_id(cr, uid, context=context) and part.team_id: + val['team_id'] = part.team_id.id sale_note = self.get_salenote(cr, uid, ids, part.id, context=context) if sale_note: val.update({'note': sale_note}) return {'value': val} @@ -467,6 +449,8 @@ class sale_order(osv.osv): def test_no_product(self, cr, uid, order, context): for line in order.order_line: + if line.state == 'cancel': + continue if line.product_id and (line.product_id.type != 'service'): return False return True @@ -548,15 +532,13 @@ class sale_order(osv.osv): context = {} sale_order_line_obj = self.pool.get('sale.order.line') account_invoice_obj = self.pool.get('account.invoice') - procurement_obj = self.pool.get('procurement.order') for sale in self.browse(cr, uid, ids, context=context): for inv in sale.invoice_ids: if inv.state not in ('draft', 'cancel'): raise UserError(_('Cannot cancel this sales order!') + ':' + _('First cancel all invoices attached to this sales order.')) inv.signal_workflow('invoice_cancel') - procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in sale.order_line],[])) - sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], - {'state': 'cancel'}) + line_ids = [l.id for l in sale.order_line if l.state != 'cancel'] + sale_order_line_obj.button_cancel(cr, uid, line_ids, context=context) self.write(cr, uid, ids, {'state': 'cancel'}) return True @@ -568,14 +550,14 @@ class sale_order(osv.osv): def action_wait(self, cr, uid, ids, context=None): context = context or {} for o in self.browse(cr, uid, ids): - if not o.order_line: + if not any(line.state != 'cancel' for line in o.order_line): raise UserError(_('You cannot confirm a sales order which has no line.')) noprod = self.test_no_product(cr, uid, o, context) if (o.order_policy == 'manual') or noprod: self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) else: self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)}) - self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line]) + self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line if x.state != 'cancel']) return True def action_quotation_send(self, cr, uid, ids, context=None): @@ -614,7 +596,7 @@ class sale_order(osv.osv): def action_done(self, cr, uid, ids, context=None): for order in self.browse(cr, uid, ids, context=context): - self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line], {'state': 'done'}, context=context) + self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], {'state': 'done'}, context=context) return self.write(cr, uid, ids, {'state': 'done'}, context=context) def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): @@ -646,7 +628,7 @@ class sale_order(osv.osv): sale_line_obj = self.pool.get('sale.order.line') res = [] for order in self.browse(cr, uid, ids, context=context): - res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line], context=context)) + res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], context=context)) return any(res) def action_ignore_delivery_exception(self, cr, uid, ids, context=None): @@ -673,6 +655,8 @@ class sale_order(osv.osv): order.write({'procurement_group_id': group_id}) for line in order.order_line: + if line.state == 'cancel': + continue #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate) if line.procurement_ids: #first check them to see if they are in exception or not (one of the related moves is cancelled) @@ -754,6 +738,8 @@ class sale_order(osv.osv): def test_procurements_done(self, cr, uid, ids, context=None): for sale in self.browse(cr, uid, ids, context=context): for line in sale.order_line: + if line.state == 'cancel': + continue if not all([x.state == 'done' for x in line.procurement_ids]): return False return True @@ -761,6 +747,8 @@ class sale_order(osv.osv): def test_procurements_except(self, cr, uid, ids, context=None): for sale in self.browse(cr, uid, ids, context=context): for line in sale.order_line: + if line.state == 'cancel': + continue if any([x.state == 'cancel' for x in line.procurement_ids]): return True return False @@ -957,9 +945,12 @@ class sale_order_line(osv.osv): return create_ids def button_cancel(self, cr, uid, ids, context=None): - for line in self.browse(cr, uid, ids, context=context): + lines = self.browse(cr, uid, ids, context=context) + for line in lines: if line.invoiced: raise UserError(_('You cannot cancel a sales order line that has already been invoiced.')) + procurement_obj = self.pool['procurement.order'] + procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in lines], []), context=context) return self.write(cr, uid, ids, {'state': 'cancel'}) def button_confirm(self, cr, uid, ids, context=None): @@ -1144,32 +1135,12 @@ class mail_compose_message(osv.Model): class account_invoice(osv.Model): _inherit = 'account.invoice' - def _get_default_team_id(self, cr, uid, context=None): - """ Gives default team by checking if present in the context """ - team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False - return team_id - - def _resolve_team_id_from_context(self, cr, uid, context=None): - """ Returns ID of team based on the value of 'team_id' - context key, or None if it cannot be resolved to a single - Sales Team. - """ - if context is None: - context = {} - if type(context.get('default_team_id')) in (int, long): - return context.get('default_team_id') - if isinstance(context.get('default_team_id'), basestring): - team_ids = self.pool.get('crm.team').name_search(cr, uid, name=context['default_team_id'], context=context) - if len(team_ids) == 1: - return int(team_ids[0][0]) - return None - _columns = { 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'), } _defaults = { - 'team_id': lambda self, cr, uid, c=None: self._get_default_team_id(cr, uid, context=c) + 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c), } def confirm_paid(self, cr, uid, ids, context=None): diff --git a/addons/sale/test/canceled_lines_order.yml b/addons/sale/test/canceled_lines_order.yml new file mode 100644 index 0000000000000000000000000000000000000000..c608b686d537e51b12ea11e55aef28a5ff10fca0 --- /dev/null +++ b/addons/sale/test/canceled_lines_order.yml @@ -0,0 +1,60 @@ +- + I create a draft Sale Order with 2 lines but 1 canceled in order to check if the canceled lines are not considered in the logic +- + !record {model: sale.order, id: sale_order_cl_2}: + partner_id: base.res_partner_15 + partner_invoice_id: base.res_partner_address_25 + partner_shipping_id: base.res_partner_address_25 + pricelist_id: product.list0 + order_policy: manual +- + !record {model: sale.order.line, id: sale_order_cl_2_line_1}: + order_id: sale_order_cl_2 + product_id: product.product_product_27 + product_uom_qty: 1 + product_uom: 1 + price_unit: 3645 + name: 'Laptop Customized' +- + !record {model: sale.order.line, id: sale_order_cl_2_line_2}: + order_id: sale_order_cl_2 + product_id: product.product_product_12 + product_uom_qty: 1 + product_uom: 1 + price_unit: 12.50 + name: 'Mouse, Wireless' +- + I cancel the first line +- + !python {model: sale.order.line, id: sale_order_cl_2_line_1}: | + self.button_cancel() +- + I confirm the sale order +- + !workflow {model: sale.order, action: order_confirm, ref: sale_order_cl_2} +- + Invoice the whole sale order +- + !python {model: sale.advance.payment.inv}: | + ctx = context.copy() + ctx.update({"active_model": 'sale.order', + "active_ids": [ref("sale_order_cl_2")], + "active_id":ref("sale_order_cl_2")}) + pay_id = self.create(cr, uid, {'advance_payment_method': 'all'}) + self.create_invoices(cr, uid, [pay_id], context=ctx) +- + I check the invoice +- + !python {model: sale.order, id: sale_order_cl_2}: | + invoice = self.invoice_ids + assert len(invoice.invoice_line_ids) == 1, "Only 1 line should be invoiced because the other one is canceled, got %d" % len(invoice.invoice_line_ids) +- + I set the sale to done +- + !python {model: sale.order, id: sale_order_cl_2}: | + self.action_done() +- + And check if the canceled line is still canceled +- + !assert {model: sale.order.line, id: sale_order_cl_2_line_1, string: The canceled line should still be canceled}: + - state == 'cancel' diff --git a/addons/sale/test/manual_order_policy.yml b/addons/sale/test/manual_order_policy.yml index ca3fc37f26cbbc3998491ba05850556c8b17b557..a00b0b4fada61c5be9c5351500677c9b29518b84 100644 --- a/addons/sale/test/manual_order_policy.yml +++ b/addons/sale/test/manual_order_policy.yml @@ -7,7 +7,7 @@ - !python {model: sale.order}: | sale_order = self.browse(cr, uid, ref("sale_order_2")) - assert len(sale_order.invoice_ids) == False, "Invoice should not created." + assert len(sale_order.invoice_ids) == 0, "Invoice should not created." - I create advance invoice where type is 'Fixed Price'. - diff --git a/addons/sale/wizard/sale_line_invoice.py b/addons/sale/wizard/sale_line_invoice.py index 579a8e83ff54a9c376316a2737ce9efe807304da..d20c8f909a820c49598579bc2d3db1cec97c6f53 100644 --- a/addons/sale/wizard/sale_line_invoice.py +++ b/addons/sale/wizard/sale_line_invoice.py @@ -103,7 +103,7 @@ class sale_order_line_make_invoice(osv.osv_memory): sales_order_obj.message_post(cr, uid, [order.id], body=_("Invoice created"), context=context) data_sale = sales_order_obj.browse(cr, uid, order.id, context=context) for line in data_sale.order_line: - if not line.invoiced: + if not line.invoiced and line.state != 'cancel': flag = False break if flag: diff --git a/addons/sale_layout/security/ir.model.access.csv b/addons/sale_layout/security/ir.model.access.csv index 10a5f7b06b0a238baf97a640db01fcb1f0f40630..9ec03a6e37840f9bbf096c80e1bd430c38d3dcce 100644 --- a/addons/sale_layout/security/ir.model.access.csv +++ b/addons/sale_layout/security/ir.model.access.csv @@ -1,6 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink report_layout_category_1,report_layout_category_1,model_sale_layout_category,base.group_sale_manager,1,1,1,1 report_layout_category_2,report_layout_category_2,model_sale_layout_category,account.group_account_manager,1,1,1,1 -report_layout_category_3,report_layout_category_3,model_sale_layout_category,base.group_sale_salesman,1,1,1,O +report_layout_category_3,report_layout_category_3,model_sale_layout_category,base.group_sale_salesman,1,1,1,0 report_layout_category_4,report_layout_category_4,model_sale_layout_category,base.group_sale_salesman_all_leads,1,1,1,0 report_layout_category_5,report_layout_category_5,model_sale_layout_category,account.group_account_invoice,1,0,0,0 diff --git a/addons/sale_margin/sale_margin.py b/addons/sale_margin/sale_margin.py index 9a20e736994ad3438c38ad8cc00877551233d167..68917daf38f9aa6bb751411679a920bbfdf29c72 100644 --- a/addons/sale_margin/sale_margin.py +++ b/addons/sale_margin/sale_margin.py @@ -19,6 +19,7 @@ ############################################################################## from openerp.osv import fields, osv +import openerp.addons.decimal_precision as dp class sale_order_line(osv.osv): _inherit = "sale.order.line" @@ -48,17 +49,20 @@ class sale_order_line(osv.osv): return res def _product_margin(self, cr, uid, ids, field_name, arg, context=None): + cur_obj = self.pool.get('res.currency') res = {} for line in self.browse(cr, uid, ids, context=context): + cur = line.order_id.pricelist_id.currency_id res[line.id] = 0 if line.product_id: - res[line.id] = round(line.price_subtotal - ((line.purchase_price or line.product_id.standard_price) * line.product_uos_qty), 2) + tmp_margin = line.price_subtotal - ((line.purchase_price or line.product_id.standard_price) * line.product_uos_qty) + res[line.id] = cur_obj.round(cr, uid, cur, tmp_margin) return res _columns = { - 'margin': fields.function(_product_margin, string='Margin', + 'margin': fields.function(_product_margin, string='Margin', digits_compute= dp.get_precision('Product Price'), store = True), - 'purchase_price': fields.float('Cost Price', digits=(16,2)) + 'purchase_price': fields.float('Cost Price', digits_compute= dp.get_precision('Product Price')) } @@ -70,6 +74,8 @@ class sale_order(osv.osv): for sale in self.browse(cr, uid, ids, context=context): result[sale.id] = 0.0 for line in sale.order_line: + if line.state == 'cancel': + continue result[sale.id] += line.margin or 0.0 return result @@ -83,5 +89,5 @@ class sale_order(osv.osv): 'margin': fields.function(_product_margin, string='Margin', help="It gives profitability by calculating the difference between the Unit Price and the cost price.", store={ 'sale.order.line': (_get_order, ['margin', 'purchase_price'], 20), 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 20), - }), + }, digits_compute= dp.get_precision('Product Price')), } diff --git a/addons/sale_mrp/sale_mrp.py b/addons/sale_mrp/sale_mrp.py index 082323ad580c31c7d50a010a2aaa6ec33f22c9ee..acc0db657429749eed4f12d30350b36c25f03b47 100644 --- a/addons/sale_mrp/sale_mrp.py +++ b/addons/sale_mrp/sale_mrp.py @@ -100,3 +100,13 @@ class stock_move(osv.osv): if res and move.procurement_id and move.procurement_id.property_ids: res['property_ids'] = [(6, 0, [x.id for x in move.procurement_id.property_ids])] return res + + def _action_explode(self, cr, uid, move, context=None): + """ Explodes pickings. + @param move: Stock moves + @return: True + """ + if context is None: + context = {} + property_ids = map(int, move.procurement_id.sale_line_id.property_ids or []) + return super(stock_move, self)._action_explode(cr, uid, move, context=dict(context, property_ids=property_ids)) diff --git a/addons/sale_mrp/tests/test_move_explode.py b/addons/sale_mrp/tests/test_move_explode.py index 88dab7135b18a86c75f2d8d2c6f14fcda7960796..ff9cfe0feef3ac2d1b3af5f8c33044342e900fe3 100644 --- a/addons/sale_mrp/tests/test_move_explode.py +++ b/addons/sale_mrp/tests/test_move_explode.py @@ -33,6 +33,7 @@ class TestMoveExplode(common.TransactionCase): self.sale_order_line = self.registry('sale.order.line') self.sale_order = self.registry('sale.order') self.mrp_bom = self.registry('mrp.bom') + self.product = self.registry('product.product') #product that has a phantom bom self.product_bom_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_3')[1] @@ -40,9 +41,24 @@ class TestMoveExplode(common.TransactionCase): self.bom_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_bom_9')[1] #partner agrolait self.partner_id = self.ir_model_data.get_object_reference(cr, uid, 'base', 'res_partner_1')[1] + #bom: PC Assemble (with property: DDR 512MB) + self.bom_prop_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_bom_property_0')[1] + + self.template_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_3_product_template')[1] + #property: DDR 512MB + self.mrp_property_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_property_0')[1] + #product: RAM SR2 + self.product_bom_prop_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_14')[1] + #phantom bom for RAM SR2 with three lines containing properties + self.bom_prop_line_id = self.ir_model_data.get_object_reference(cr, uid, 'mrp', 'mrp_bom_property_line')[1] + #product: iPod included in the phantom bom + self.product_A_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_11')[1] + #product: Mouse, Wireless included in the phantom bom + self.product_B_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_12')[1] + def test_00_sale_move_explode(self): - """check that when creating a sale order with a product that has a phantom BoM, move explode into content of the + """check that when creating a sale order with a product that has a phantom BoM, move explode into content of the BoM""" cr, uid, context = self.cr, self.uid, {} #create sale order with one sale order line containing product with a phantom bom @@ -57,3 +73,19 @@ class TestMoveExplode(common.TransactionCase): bom = self.mrp_bom.browse(cr, uid, self.bom_id, context=context) bom_component_length = self.mrp_bom._bom_explode(cr, uid, bom, self.product_bom_id, 1, []) self.assertEqual(len(move_ids), len(bom_component_length[0])) + + def test_00_bom_find(self): + """Check that _bom_find searches the bom corresponding to the properties passed or takes the bom with the smallest + sequence.""" + cr, uid, context = self.cr, self.uid, {} + res_id = self.mrp_bom._bom_find(cr, uid, product_tmpl_id=self.template_id, product_id=None, properties=[self.mrp_property_id], context=context) + self.assertEqual(res_id, self.bom_prop_id) + + def test_00_bom_explode(self): + """Check that _bom_explode only takes the lines with the right properties.""" + cr, uid, context = self.cr, self.uid, {} + bom = self.mrp_bom.browse(cr, uid, self.bom_prop_line_id) + product = self.product.browse(cr, uid, self.product_bom_prop_id) + res = self.mrp_bom._bom_explode(cr, uid, bom, product, 1, properties=[self.mrp_property_id], context=context) + res = set([p['product_id'] for p in res[0]]) + self.assertEqual(res, set([self.product_A_id, self.product_B_id])) diff --git a/addons/sale_order_dates/sale_order_dates.py b/addons/sale_order_dates/sale_order_dates.py index 9b341e699742ebf6c18e4df77ed0cfa75e5b44ef..2a2416d112503cfd6eae941b69ae396845750a3d 100644 --- a/addons/sale_order_dates/sale_order_dates.py +++ b/addons/sale_order_dates/sale_order_dates.py @@ -61,6 +61,8 @@ class sale_order_dates(osv.osv): dates_list = [] order_datetime = datetime.strptime(order.date_order, DEFAULT_SERVER_DATETIME_FORMAT) for line in order.order_line: + if line.state == 'cancel': + continue dt = order_datetime + timedelta(days=line.delay or 0.0) dt_s = dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) dates_list.append(dt_s) diff --git a/addons/sale_stock/__openerp__.py b/addons/sale_stock/__openerp__.py index 665e423639f736de257e0af1398962a023e0520f..4bdd595d3ce1695a258eb826bc1074332422f60d 100644 --- a/addons/sale_stock/__openerp__.py +++ b/addons/sale_stock/__openerp__.py @@ -64,6 +64,7 @@ You can choose flexible invoicing methods: 'test/picking_order_policy.yml', 'test/prepaid_order_policy.yml', 'test/sale_order_onchange.yml', + 'test/sale_order_canceled_line.yml', ], 'installable': True, 'auto_install': True, diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 0ef83c5f50734c526806bfec12ca349d1754634f..ef6edf9e5d93b5cdc6075c16b00339265a0bfae5 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -196,6 +196,8 @@ class sale_order(osv.osv): def has_stockable_products(self, cr, uid, ids, *args): for order in self.browse(cr, uid, ids): for order_line in order.order_line: + if order_line.state == 'cancel': + continue if order_line.product_id and order_line.product_id.type in ('product', 'consu'): return True return False @@ -280,6 +282,29 @@ class sale_order_line(osv.osv): return {'value': result, 'warning': warning} + def _check_routing(self, cr, uid, ids, product, warehouse_id, context=None): + """ Verify the route of the product based on the warehouse + return True if the product availibility in stock does not need to be verified + """ + is_available = False + if warehouse_id: + warehouse = self.pool['stock.warehouse'].browse(cr, uid, warehouse_id, context=context) + for product_route in product.route_ids: + if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id: + is_available = True + break + else: + try: + mto_route_id = self.pool['stock.warehouse']._get_mto_route(cr, uid, context=context) + except osv.except_osv: + # if route MTO not found in ir_model_data, we treat the product as in MTS + mto_route_id = False + if mto_route_id: + for product_route in product.route_ids: + if product_route.id == mto_route_id: + is_available = True + break + return is_available def product_id_change_with_wh(self, cr, uid, ids, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, @@ -287,7 +312,6 @@ class sale_order_line(osv.osv): context = context or {} product_uom_obj = self.pool.get('product.uom') product_obj = self.pool.get('product.product') - warehouse_obj = self.pool['stock.warehouse'] warning = {} #UoM False due to hack which makes sure uom changes price, ... in product_id_change res = self.product_id_change(cr, uid, ids, pricelist, product, qty=qty, @@ -316,28 +340,11 @@ class sale_order_line(osv.osv): warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or '' if product_obj.type == 'product': - #determine if the product is MTO or not (for a further check) - isMto = False - if warehouse_id: - warehouse = warehouse_obj.browse(cr, uid, warehouse_id, context=context) - for product_route in product_obj.route_ids: - if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id: - isMto = True - break - else: - try: - mto_route_id = warehouse_obj._get_mto_route(cr, uid, context=context) - except: - # if route MTO not found in ir_model_data, we treat the product as in MTS - mto_route_id = False - if mto_route_id: - for product_route in product_obj.route_ids: - if product_route.id == mto_route_id: - isMto = True - break + #determine if the product needs further check for stock availibility + is_available = self._check_routing(cr, uid, ids, product_obj, warehouse_id, context=context) #check if product is available, and if not: raise a warning, but do this only for products that aren't processed in MTO - if not isMto: + if not is_available: uom_record = False if uom: uom_record = product_uom_obj.browse(cr, uid, uom, context=context) @@ -362,6 +369,14 @@ class sale_order_line(osv.osv): res.update({'warning': warning}) return res + def button_cancel(self, cr, uid, ids, context=None): + lines = self.browse(cr, uid, ids, context=context) + for procurement in lines.mapped('procurement_ids'): + for move in procurement.move_ids: + if move.state == 'done' and not move.scrapped: + raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line which is linked to a stock move already done.')) + return super(sale_order_line, self).button_cancel(cr, uid, ids, context=context) + class stock_move(osv.osv): _inherit = 'stock.move' @@ -388,7 +403,7 @@ class stock_move(osv.osv): if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.order_policy == 'picking': sale_order = move.procurement_id.sale_line_id.order_id return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id - elif move.picking_id.sale_id: + elif move.picking_id.sale_id and context.get('inv_type') in ('out_invoice', 'out_refund'): # In case of extra move, it is better to use the same data as the original moves sale_order = move.picking_id.sale_id return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id @@ -396,7 +411,7 @@ class stock_move(osv.osv): def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None): res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context) - if move.procurement_id and move.procurement_id.sale_line_id: + if inv_type in ('out_invoice', 'out_refund') and move.procurement_id and move.procurement_id.sale_line_id: sale_line = move.procurement_id.sale_line_id res['invoice_line_tax_ids'] = [(6, 0, [x.id for x in sale_line.tax_id])] res['account_analytic_id'] = sale_line.order_id.project_id and sale_line.order_id.project_id.id or False @@ -420,6 +435,10 @@ class stock_move(osv.osv): extra_move_tax[move.picking_id, move.product_id] = [(6, 0, [x.id for x in move.procurement_id.sale_line_id.tax_id])] return (is_extra_move, extra_move_tax) + def _get_taxes(self, cr, uid, move, context=None): + if move.procurement_id.sale_line_id.tax_id: + return [tax.id for tax in move.procurement_id.sale_line_id.tax_id] + return super(stock_move, self)._get_taxes(cr, uid, move, context=context) class stock_location_route(osv.osv): _inherit = "stock.location.route" @@ -474,6 +493,7 @@ class stock_picking(osv.osv): 'user_id': sale.user_id.id, 'team_id': sale.team_id.id, 'name': sale.client_order_ref or '', + 'comment': sale.note, }) return inv_vals diff --git a/addons/sale_stock/test/sale_order_canceled_line.yml b/addons/sale_stock/test/sale_order_canceled_line.yml new file mode 100644 index 0000000000000000000000000000000000000000..4e229fc4d66705bb5b4938cf599c76e5b3300ec7 --- /dev/null +++ b/addons/sale_stock/test/sale_order_canceled_line.yml @@ -0,0 +1,45 @@ +- + I create a draft Sale Order with 2 lines but 1 canceled in order to check if the canceled lines are not considered in the logic +- + !record {model: sale.order, id: sale_order_cl_3}: + partner_id: base.res_partner_15 + partner_invoice_id: base.res_partner_address_25 + partner_shipping_id: base.res_partner_address_25 + pricelist_id: product.list0 + order_policy: manual +- + !record {model: sale.order.line, id: sale_order_cl_3_line_1}: + order_id: sale_order_cl_3 + product_id: product.product_product_27 + product_uom_qty: 1 + product_uom: 1 + price_unit: 3645 + name: 'Laptop Customized' +- + !record {model: sale.order.line, id: sale_order_cl_3_line_2}: + order_id: sale_order_cl_3 + product_id: product.product_product_12 + product_uom_qty: 1 + product_uom: 1 + price_unit: 12.50 + name: 'Mouse, Wireless' +- + I cancel the first line +- + !python {model: sale.order.line, id: sale_order_cl_3_line_1}: | + self.button_cancel() +- + I confirm the sale order +- + !workflow {model: sale.order, action: order_confirm, ref: sale_order_cl_3} +- + I check that no procurement has been generated for the canceled line +- + !assert {model: sale.order.line, id: sale_order_cl_3_line_1, string: The canceled line should not have a procurement}: + - not procurement_ids +- + I check that we have only 1 stock move, for the not canceled line +- + !python {model: sale.order, id: sale_order_cl_3}: | + moves = self.picking_ids.mapped('move_lines') + assert len(moves) == 1, "We should have 1 move, got %s" % len(moves) diff --git a/addons/sales_team/sales_team.py b/addons/sales_team/sales_team.py index 24c5519e977f59be70edfd4a550e15c4cda5a782..2ad65c3c2d051c0be908a2d20e386a3ab9f5ffbf 100644 --- a/addons/sales_team/sales_team.py +++ b/addons/sales_team/sales_team.py @@ -34,6 +34,28 @@ class crm_team(osv.Model): def get_full_name(self, cr, uid, ids, field_name, arg, context=None): return dict(self.name_get(cr, uid, ids, context=context)) + def _get_default_team_id(self, cr, uid, user_id=None, context=None): + team_id = self._resolve_team_id_from_context(cr, uid, context=context) or False + if not team_id: + team_ids = self.search(cr, uid, [('member_ids', '=', user_id or uid)], limit=1, context=context) + team_id = team_ids[0] if team_ids else False + return team_id + + def _resolve_team_id_from_context(self, cr, uid, context=None): + """ Returns ID of team based on the value of 'default_team_id' + context key, or None if it cannot be resolved to a single + Sales Team. + """ + if context is None: + context = {} + if type(context.get('default_team_id')) in (int, long): + return context.get('default_team_id') + if isinstance(context.get('default_team_id'), basestring): + team_ids = self.name_search(cr, uid, name=context['default_team_id'], context=context) + if len(team_ids) == 1: + return int(team_ids[0][0]) + return None + _columns = { 'name': fields.char('Sales Team', size=64, required=True, translate=True), 'complete_name': fields.function(get_full_name, type='char', size=256, readonly=True, store=True, string="Full Name"), diff --git a/addons/share/wizard/share_wizard.py b/addons/share/wizard/share_wizard.py index 4813dc865bf4c3b1ad032732e033f1503c6b2294..b9b6cf4f6e923dd1a9141117d25da7451b51e700 100644 --- a/addons/share/wizard/share_wizard.py +++ b/addons/share/wizard/share_wizard.py @@ -493,7 +493,7 @@ class share_wizard(osv.TransientModel): # already granted for dummy, model in fields_relations: # mail.message is transversal: it should not received directly the access rights - if model.model in ['mail.message', 'mail.notification']: continue + if model.model in ['mail.message', 'mail.notification', 'res.company']: continue values = { 'name': _('Copied access for sharing'), 'group_id': group_id, @@ -600,8 +600,8 @@ class share_wizard(osv.TransientModel): # other groups, so we duplicate if needed rule = self._check_personal_rule_or_duplicate(cr, group_id, rule, context=context) eval_ctx = rule_obj._eval_context_for_combinations() - org_domain = expression.normalize_domain(eval(rule.domain_force, eval_ctx)) - new_clause = expression.normalize_domain(eval(domain, eval_ctx)) + org_domain = expression.normalize_domain(safe_eval(rule.domain_force, eval_ctx)) + new_clause = expression.normalize_domain(safe_eval(domain, eval_ctx)) combined_domain = expression.AND([new_clause, org_domain]) rule.write({'domain_force': combined_domain, 'name': rule.name + _('(Modified)')}) _logger.debug("Combining sharing rule %s on model %s with domain: %s", rule.id, model_id, domain) @@ -625,7 +625,7 @@ class share_wizard(osv.TransientModel): if domain: for rel_field, model in fields_relations: # mail.message is transversal: it should not received directly the access rights - if model.model in ['mail.message', 'mail.notification']: continue + if model.model in ['mail.message', 'mail.notification', 'res.company']: continue related_domain = [] if not rel_field: continue for element in domain: diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py index 7f3a1534991655f5df231af4bf3bfc8114717063..32a4a5c4817650539097788d8708f57c04072fc2 100644 --- a/addons/stock/procurement.py +++ b/addons/stock/procurement.py @@ -510,7 +510,7 @@ class procurement_order(osv.osv): qty -= subtract_qty[op.id] qty_rounded = float_round(qty, precision_rounding=op.product_uom.rounding) - if qty_rounded >= 0: + if qty_rounded > 0: proc_id = procurement_obj.create(cr, uid, self._prepare_orderpoint_procurement(cr, uid, op, qty_rounded, context=context), context=context) diff --git a/addons/stock/product.py b/addons/stock/product.py index 0129ae84965030f87ac52541d1e74e0d434d8720..748a41f751d35cc8df72e6050ec53af52768c399 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -280,7 +280,9 @@ class product_product(osv.osv): } def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): - res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu) + res = super(product_product, self).fields_view_get( + cr, uid, view_id=view_id, view_type=view_type, context=context, + toolbar=toolbar, submenu=submenu) if context is None: context = {} if ('location' in context) and context['location']: diff --git a/addons/stock/stock.py b/addons/stock/stock.py index aa31f70caeda2584091938ee55c45c96f082f9d8..471b1282821a556ec03d8db66b8573d9e4e8098c 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -794,7 +794,7 @@ class stock_picking(osv.osv): 'name': fields.char('Reference', select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=False), 'origin': fields.char('Source Document', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document", select=True), 'backorder_id': fields.many2one('stock.picking', 'Back Order of', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.", select=True, copy=False), - 'note': fields.text('Notes', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}), + 'note': fields.text('Notes'), 'move_type': fields.selection([('direct', 'Partial'), ('one', 'All at once')], 'Delivery Method', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="It specifies goods to be deliver partially or all at once"), 'state': fields.function(_state_get, type="selection", copy=False, store={ @@ -953,6 +953,12 @@ class stock_picking(osv.osv): return super(stock_picking, self).unlink(cr, uid, ids, context=context) def write(self, cr, uid, ids, vals, context=None): + if vals.get('move_lines') and not vals.get('pack_operation_ids'): + # pack operations are directly dependant of move lines, it needs to be recomputed + pack_operation_obj = self.pool['stock.pack.operation'] + existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', ids)], context=context) + if existing_package_ids: + pack_operation_obj.unlink(cr, uid, existing_package_ids, context) res = super(stock_picking, self).write(cr, uid, ids, vals, context=context) #if we changed the move lines or the pack operations, we need to recompute the remaining quantities of both if 'move_lines' in vals or 'pack_operation_ids' in vals: @@ -1621,7 +1627,7 @@ class stock_production_lot(osv.osv): # Move # ---------------------------------------------------- -class stock_move(models.Model): +class stock_move(osv.osv): _name = "stock.move" _description = "Stock Move" _order = 'picking_id, sequence, id' @@ -1642,11 +1648,12 @@ class stock_move(models.Model): res.append((line.id, name)) return res - @api.depends('product_id', 'product_uom_qty', 'product_uom', 'product_id.uom_id') - def _quantity_normalize(self): + def _quantity_normalize(self, cr, uid, ids, name, args, context=None): uom_obj = self.pool.get('product.uom') - for m in self: - m.product_qty = uom_obj._compute_qty_obj(self.env.cr, self.env.uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, context=self.env.context) + res = {} + for m in self.browse(cr, uid, ids, context=context): + res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, context=context) + return res def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None): uom_obj = self.pool.get('product.uom') @@ -1730,6 +1737,11 @@ class stock_move(models.Model): res += [x.id for x in picking.move_lines] return res + def _get_moves_from_prod(self, cr, uid, ids, context=None): + if ids: + return self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', ids)], context=context) + return [] + def _set_product_qty(self, cr, uid, id, field, value, arg, context=None): """ The meaning of product_qty field changed lately and is now a functional field computing the quantity in the default product UoM. This code has been added to raise an error if a write is made given a value @@ -1738,9 +1750,6 @@ class stock_move(models.Model): """ raise UserError(_('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.')) - product_qty = new_fields.Float(compute='_quantity_normalize', inverse='_set_product_qty', digits=0, store=True, string='Quantity', - help='Quantity in the default UoM of the product') - _columns = { 'sequence': fields.integer('Sequence'), 'name': fields.char('Description', required=True, select=True), @@ -1749,6 +1758,8 @@ class stock_move(models.Model): 'date': fields.datetime('Date', required=True, select=True, help="Move date: scheduled date until move is done, then date of actual move processing", states={'done': [('readonly', True)]}), 'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"), 'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}), + 'product_qty': fields.function(_quantity_normalize, fnct_inv=_set_product_qty, type='float', digits=0, store=True, string='Quantity', + help='Quantity in the default UoM of the product'), 'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, states={'done': [('readonly', True)]}, help="This is the quantity of products from an inventory " @@ -2353,7 +2364,7 @@ class stock_move(models.Model): if todo: ids = self.action_confirm(cr, uid, todo, context=context) pickings = set() - procurement_ids = [] + procurement_ids = set() #Search operations that are linked to the moves operations = set() move_qty = {} @@ -2417,7 +2428,7 @@ class stock_move(models.Model): move_dest_ids.add(move.move_dest_id.id) if move.procurement_id: - procurement_ids.append(move.procurement_id.id) + procurement_ids.add(move.procurement_id.id) #unreserve the quants and make them available for other operations/moves quant_obj.quants_unreserve(cr, uid, move, context=context) @@ -2425,7 +2436,7 @@ class stock_move(models.Model): self._check_package_from_moves(cr, uid, ids, context=context) #set the move as done self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) - self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context) + self.pool.get('procurement.order').check(cr, uid, list(procurement_ids), context=context) #assign destination moves if move_dest_ids: self.action_assign(cr, uid, list(move_dest_ids), context=context) @@ -2564,6 +2575,8 @@ class stock_move(models.Model): code = 'incoming' return code + def _get_taxes(self, cr, uid, move, context=None): + return [] class stock_inventory(osv.osv): _name = "stock.inventory" @@ -2753,6 +2766,36 @@ class stock_inventory(osv.osv): vals.append(product_line) return vals + def _check_filter_product(self, cr, uid, ids, context=None): + for inventory in self.browse(cr, uid, ids, context=context): + if inventory.filter == 'none' and inventory.product_id and inventory.location_id and inventory.lot_id: + return True + if inventory.filter not in ('product', 'product_owner') and inventory.product_id: + return False + if inventory.filter != 'lot' and inventory.lot_id: + return False + if inventory.filter not in ('owner', 'product_owner') and inventory.partner_id: + return False + if inventory.filter != 'pack' and inventory.package_id: + return False + return True + + def onchange_filter(self, cr, uid, ids, filter, context=None): + to_clean = { 'value': {} } + if filter not in ('product', 'product_owner'): + to_clean['value']['product_id'] = False + if filter != 'lot': + to_clean['value']['lot_id'] = False + if filter not in ('owner', 'product_owner'): + to_clean['value']['partner_id'] = False + if filter != 'pack': + to_clean['value']['package_id'] = False + return to_clean + + _constraints = [ + (_check_filter_product, 'The selected inventory options are not coherent.', + ['filter', 'product_id', 'lot_id', 'partner_id', 'package_id']), + ] class stock_inventory_line(osv.osv): _name = "stock.inventory.line" @@ -2811,6 +2854,7 @@ class stock_inventory_line(osv.osv): _defaults = { 'product_qty': 0, + 'product_uom_id': lambda self, cr, uid, ctx=None: self.pool['ir.model.data'].get_object_reference(cr, uid, 'product', 'product_uom_unit')[1] } def create(self, cr, uid, values, context=None): diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index e4377bb3ae5018c86a9d8eab89a65e5357d3b9fe..1898233b2f87047f4c04a7b725b22e8d04661c1b 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -109,7 +109,7 @@ <group> <group> <field name="location_id" domain="[('usage','not in', ['supplier','production'])]"/> - <field name="filter" string="Inventory of" widget='radio' attrs="{'readonly': [('state', '!=', 'draft')]}"/> + <field name="filter" string="Inventory of" widget='radio' attrs="{'readonly': [('state', '!=', 'draft')]}" on_change="onchange_filter(filter)"/> </group> <group> <field name="date"/> @@ -1222,7 +1222,7 @@ <filter icon="terp-stock" string="To Do" name="future" domain="[('state','in',('assigned','confirmed','waiting'))]" help="Stock moves that are Confirmed, Available or Waiting"/> <filter icon="terp-dialog-close" string="Done" name="done" domain="[('state','=','done')]" help="Stock moves that have been processed"/> <separator/> - <filter string="Today" domain="[('date','<=',datetime.datetime.now().replace(hour=23, minute=59, second=59)),('date','>=',datetime.datetime.now().replace(hour=0, minute=0, second=0))]" help="Orders processed Today or planned for Today"/> + <filter string="Today" domain="[('date','<=', datetime.datetime.combine(context_today(), datetime.time(23,59,59))), ('date','>=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]" help="Orders processed Today or planned for Today"/> <field name="product_id"/> <field name="name" string="Location" filter_domain="['|',('location_id','ilike',self),('location_dest_id','ilike',self)]"/> <field name="partner_id" string="Partner" filter_domain="[('picking_id.partner_id','child_of',self)]"/> @@ -1436,6 +1436,7 @@ <group> <field name="code" on_change="onchange_picking_code(code)"/> <field name="return_picking_type_id"/> + <field name="barcode_nomenclature_id"/> </group> </group> <separator string="Locations"/> diff --git a/addons/stock/test/inventory.yml b/addons/stock/test/inventory.yml index a0b0f58512b73c306dece52cf2a3d23341c1f97f..4bfc70d3dd12c601719807159c3737fd6e8a3aec 100644 --- a/addons/stock/test/inventory.yml +++ b/addons/stock/test/inventory.yml @@ -9,6 +9,7 @@ - !record {model: stock.inventory, id: inventory_test0}: name: Test + filter: product product_id: inventory_product - I confirm the inventory and check that my inventory has no line, as the product is a new one. @@ -60,6 +61,7 @@ - !record {model: stock.inventory, id: inventory_test1}: name: second test inventory + filter: product product_id: inventory_product - I confirm the inventory and check that my inventory has one line, with a quantity of 10. diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py index d541338ed2e5e42f7c4441659d4f90b70292da17..63604ca2cb099d756827ee02a11461865c380e1c 100644 --- a/addons/stock/wizard/stock_change_product_qty.py +++ b/addons/stock/wizard/stock_change_product_qty.py @@ -94,8 +94,15 @@ class stock_change_product_qty(osv.osv_memory): ctx = context.copy() ctx['location'] = data.location_id.id ctx['lot_id'] = data.lot_id.id + if data.product_id.id and data.lot_id.id: + filter = 'none' + elif data.product_id.id: + filter = 'product' + else: + filter = 'none' inventory_id = inventory_obj.create(cr, uid, { 'name': _('INV: %s') % tools.ustr(data.product_id.name), + 'filter': filter, 'product_id': data.product_id.id, 'location_id': data.location_id.id, 'lot_id': data.lot_id.id}, context=context) @@ -113,3 +120,11 @@ class stock_change_product_qty(osv.osv_memory): inventory_line_obj.create(cr , uid, line_data, context=context) inventory_obj.action_done(cr, uid, [inventory_id], context=context) return {} + + def onchange_location_id(self, cr, uid, ids, location_id, product_id, context=None): + if location_id: + qty_wh = 0.0 + qty = self.pool.get('product.product')._product_available(cr, uid, [product_id], context=dict(context or {}, location=location_id)) + if product_id in qty: + qty_wh = qty[product_id]['qty_available'] + return { 'value': { 'new_quantity': qty_wh } } diff --git a/addons/stock/wizard/stock_change_product_qty_view.xml b/addons/stock/wizard/stock_change_product_qty_view.xml index 7c11189d1c39510f5c7a93f05c5ea7ab3a643cd1..4bf556fcaefdd099e9dfe2e237f1d347055d758f 100644 --- a/addons/stock/wizard/stock_change_product_qty_view.xml +++ b/addons/stock/wizard/stock_change_product_qty_view.xml @@ -7,9 +7,9 @@ <field name="arch" type="xml"> <form string="Update Product Quantity"> <group> - <field name="new_quantity" /> <field name="product_id" invisible="1"/> - <field name="location_id" groups="stock.group_locations"/> + <field name="location_id" groups="stock.group_locations" on_change="onchange_location_id(location_id, product_id, context)"/> + <field name="new_quantity"/> <field name="lot_id" context="{'search_default_product_id':product_id,'default_product_id':product_id}" groups="stock.group_production_lot"/> <p groups="stock.group_production_lot" class="oe_grey"> When you select a serial number (lot), the quantity is corrected with respect to diff --git a/addons/stock/wizard/stock_transfer_details.py b/addons/stock/wizard/stock_transfer_details.py index ed03413f5fa60101fddaf65cfacefd39a9b39c4b..437509aacf38f09f54d18757b8a7884546bae064 100644 --- a/addons/stock/wizard/stock_transfer_details.py +++ b/addons/stock/wizard/stock_transfer_details.py @@ -48,7 +48,8 @@ class stock_transfer_details(models.TransientModel): picking = self.pool.get('stock.picking').browse(cr, uid, picking_id, context=context) items = [] packs = [] - picking.do_prepare_partial() + if not picking.pack_operation_ids: + picking.do_prepare_partial() for op in picking.pack_operation_ids: item = { 'packop_id': op.id, @@ -90,7 +91,7 @@ class stock_transfer_details(models.TransientModel): 'owner_id': prod.owner_id.id, } if prod.packop_id: - prod.packop_id.write(pack_datas) + prod.packop_id.with_context(no_recompute=True).write(pack_datas) processed_ids.append(prod.packop_id.id) else: pack_datas['picking_id'] = self.picking_id.id @@ -98,8 +99,7 @@ class stock_transfer_details(models.TransientModel): processed_ids.append(packop_id.id) # Delete the others packops = self.env['stock.pack.operation'].search(['&', ('picking_id', '=', self.picking_id.id), '!', ('id', 'in', processed_ids)]) - for packop in packops: - packop.unlink() + packops.unlink() # Execute the transfer of the picking self.picking_id.do_transfer() diff --git a/addons/stock_account/product.py b/addons/stock_account/product.py index 60d93990831add4a31c8933f85066c3680bfdccc..52e1432d7af2a7b90ccc65caedcb69ba2a3d8ebd 100644 --- a/addons/stock_account/product.py +++ b/addons/stock_account/product.py @@ -60,6 +60,12 @@ class product_template(osv.osv): 'valuation': 'manual_periodic', } + def onchange_type(self, cr, uid, ids, type): + res = super(product_template, self).onchange_type(cr, uid, ids, type) + if type in ('consu', 'service'): + res = {'value': {'valuation': 'manual_periodic'}} + return res + @api.multi def _get_product_accounts(self): """ To get the stock input account, stock output account and stock journal related to product. @@ -132,6 +138,13 @@ class product_template(osv.osv): return True +class product_product(osv.osv): + _inherit = 'product.product' + def onchange_type(self, cr, uid, ids, type): + res = super(product_product, self).onchange_type(cr, uid, ids, type) + if type in ('consu', 'service'): + res = {'value': {'valuation': 'manual_periodic'}} + return res class product_category(osv.osv): _inherit = 'product.category' _columns = { diff --git a/addons/stock_account/product_view.xml b/addons/stock_account/product_view.xml index e562b508cf29887d976233c4cd3af47c940670da..02942a67919ab4cf74d04583ce8270cf59cd856a 100644 --- a/addons/stock_account/product_view.xml +++ b/addons/stock_account/product_view.xml @@ -39,9 +39,9 @@ </xpath> <xpath expr="//group[@name='properties']" position="before"> <group groups="stock_account.group_inventory_valuation" attrs="{'invisible': [('type','=','service')]}"> - <separator string="Inventory Valuation" colspan="4"/> + <separator string="Inventory Valuation" attrs="{'invisible':[('type', 'in', ('service', 'consu'))]}" colspan="4"/> <group colspan="2" col="2"> - <field name="valuation" attrs="{'readonly':[('type', '=', 'service')]}"/> + <field name="valuation" attrs="{'invisible':[('type', 'in', ('service', 'consu'))]}"/> </group> <group colspan="2" col="2"> <field name="property_stock_account_input" attrs="{'invisible':[('valuation', '!=', 'real_time')]}" diff --git a/addons/stock_account/stock.py b/addons/stock_account/stock.py index ed8d7ecaaf180b37bb4ac615295e2752b9bc70d4..0d379299d0a40952220bbc9caf4dbb05aa360df1 100644 --- a/addons/stock_account/stock.py +++ b/addons/stock_account/stock.py @@ -147,6 +147,9 @@ class stock_move(osv.osv): if move.product_uos: uos_id = move.product_uos.id quantity = move.product_uos_qty + + taxes_ids = self._get_taxes(cr, uid, move, context=context) + return { 'name': move.name, 'account_id': account_id, @@ -154,6 +157,7 @@ class stock_move(osv.osv): 'uos_id': uos_id, 'quantity': quantity, 'price_unit': self._get_price_unit_invoice(cr, uid, move, inv_type), + 'invoice_line_tax_ids': [(6, 0, taxes_ids)], 'discount': 0.0, 'account_analytic_id': False, 'move_id': move.id, @@ -306,6 +310,7 @@ class stock_picking(osv.osv): move_obj = self.pool.get('stock.move') invoices = {} is_extra_move, extra_move_tax = move_obj._get_moves_taxes(cr, uid, moves, context=context) + product_price_unit = {} for move in moves: company = move.company_id origin = move.picking_id.name @@ -331,8 +336,12 @@ class stock_picking(osv.osv): invoice_line_vals = move_obj._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context) invoice_line_vals['invoice_id'] = invoices[key] invoice_line_vals['origin'] = origin + if not is_extra_move[move.id]: + product_price_unit[invoice_line_vals['product_id']] = invoice_line_vals['price_unit'] + if is_extra_move[move.id] and invoice_line_vals['product_id'] in product_price_unit: + invoice_line_vals['price_unit'] = product_price_unit[invoice_line_vals['product_id']] if is_extra_move[move.id] and extra_move_tax[move.picking_id, move.product_id]: - invoice_line_vals['invoice_line_tax_id'] = extra_move_tax[move.picking_id, move.product_id] + invoice_line_vals['invoice_line_tax_ids'] = extra_move_tax[move.picking_id, move.product_id] move_obj._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context) move_obj.write(cr, uid, move.id, {'invoice_state': 'invoiced'}, context=context) diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index 50148549fd1a7510a76d03495f70e6fab72c24c0..e98eeae4ff5efbd94fd42e92fe0fd6402da9f142 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -308,9 +308,9 @@ class stock_quant(osv.osv): valuation_amount = context.get('force_valuation_amount') else: if move.product_id.cost_method == 'average': - valuation_amount = move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal' and cost or move.product_id.standard_price + valuation_amount = cost if move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal' else move.product_id.standard_price else: - valuation_amount = move.product_id.cost_method == 'real' and cost or move.product_id.standard_price + valuation_amount = cost if move.product_id.cost_method == 'real' else move.product_id.standard_price #the standard_price of the product may be in another decimal precision, or not compatible with the coinage of #the company currency... so we need to use round() before creating the accounting entries. valuation_amount = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty) @@ -392,7 +392,8 @@ class stock_move(osv.osv): average_valuation_price += q.qty * q.cost average_valuation_price = average_valuation_price / move.product_qty # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=context) + ctx = dict(context, force_company=move.company_id.id) + product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=ctx) self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context) def product_price_update_before_done(self, cr, uid, ids, context=None): @@ -417,7 +418,8 @@ class stock_move(osv.osv): new_std_price = ((amount_unit * product_avail) + (move.price_unit * move.product_qty)) / (product_avail + move.product_qty) tmpl_dict[prod_tmpl_id] += move.product_qty # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products - product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=context) + ctx = dict(context or {}, force_company=move.company_id.id) + product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=ctx) def product_price_update_after_done(self, cr, uid, ids, context=None): ''' diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py index a5f6774db53b87fbe128210e89f48cf095d434d6..99aa9511a9dd3852b4721db05efa7d95925ed323 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/wizard/stock_valuation_history.py @@ -139,7 +139,7 @@ class stock_history(osv.osv): product_product ON product_product.id = stock_move.product_id LEFT JOIN product_template ON product_template.id = product_product.product_tmpl_id - WHERE stock_move.state = 'done' AND dest_location.usage in ('internal', 'transit') + WHERE quant.qty>0 AND stock_move.state = 'done' AND dest_location.usage in ('internal', 'transit') AND ((source_location.company_id is null and dest_location.company_id is not null) or (source_location.company_id is not null and dest_location.company_id is null) or source_location.company_id != dest_location.company_id) ) UNION @@ -173,7 +173,7 @@ class stock_history(osv.osv): product_product ON product_product.id = stock_move.product_id LEFT JOIN product_template ON product_template.id = product_product.product_tmpl_id - WHERE stock_move.state = 'done' AND source_location.usage in ('internal', 'transit') + WHERE quant.qty>0 AND stock_move.state = 'done' AND source_location.usage in ('internal', 'transit') AND ((dest_location.company_id is null and source_location.company_id is not null) or (dest_location.company_id is not null and source_location.company_id is null) or dest_location.company_id != source_location.company_id) )) diff --git a/addons/stock_dropshipping/__init__.py b/addons/stock_dropshipping/__init__.py index 15df586b7f073dcfd3a8a0feceaa3bb3af2b5fc7..f4a62bbcd4961905514d2afc732838ded039a0f7 100644 --- a/addons/stock_dropshipping/__init__.py +++ b/addons/stock_dropshipping/__init__.py @@ -18,4 +18,5 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## +import stock_dropshipping import wizard diff --git a/addons/stock_dropshipping/stock_dropshipping.py b/addons/stock_dropshipping/stock_dropshipping.py new file mode 100644 index 0000000000000000000000000000000000000000..2fe37ea8565fab5834e90ac77107ee03af5a157d --- /dev/null +++ b/addons/stock_dropshipping/stock_dropshipping.py @@ -0,0 +1,22 @@ +# coding: utf-8 + +from openerp import models, api + + +class sale_order_line(models.Model): + _inherit = 'sale.order.line' + + @api.multi + def _check_routing(self, product, warehouse): + """ skip stock verification if the route goes from supplier to customer + As the product never goes in stock, no need to verify it's availibility + """ + res = super(sale_order_line, self)._check_routing(product, warehouse) + if not res: + for line in self: + for pull_rule in line.route_id.pull_ids: + if (pull_rule.picking_type_id.default_location_src_id.usage == 'supplier' and + pull_rule.picking_type_id.default_location_dest_id.usage == 'customer'): + res = True + break + return res diff --git a/addons/stock_dropshipping/test/crossdock.yml b/addons/stock_dropshipping/test/crossdock.yml index 5ad1f1db187f2b09692c8efbb338b8adbaa4b258..2c79a1a3754bd5255aba98e5ced8dc73abc7944e 100644 --- a/addons/stock_dropshipping/test/crossdock.yml +++ b/addons/stock_dropshipping/test/crossdock.yml @@ -1,42 +1,58 @@ -- - Create new product without any routes -- - !record {model: product.product, id: cross_shop_product}: - name: PCE - type: product - categ_id: product.product_category_1 - list_price: 100.0 - standard_price: 70.0 - seller_ids: - - delay: 1 - name: base.res_partner_2 - min_qty: 2.0 - qty: 5.0 - type: product - uom_id: product.product_uom_unit - uom_po_id: product.product_uom_unit -- - Create a sales order with a line of 100 PCE incoming shipment, with route_id crossdock shipping. -- - !record {model: sale.order, id: sale_order_crossdock_shpng}: - partner_id: base.res_partner_4 - note: Create Sales order - order_line: - - product_id: cross_shop_product - product_uom_qty: 100.00 +- + Create a supplier. +- + !record {model: res.partner, id: supplier_crossdock}: + name: Crossdocking supplier +- + Use the warehouse created in cancellation_propagated.yml. +- + !python {model: stock.warehouse, id: wh_pps}: | + assert self.crossdock_route_id.active, 'Crossdock route is not active' +- + Create new product without any routes. +- + !record {model: product.product, id: cross_shop_product}: + name: PCE + type: product + categ_id: product.product_category_1 + list_price: 100.0 + standard_price: 70.0 + seller_ids: + - delay: 1 + name: supplier_crossdock + min_qty: 2.0 + qty: 5.0 + type: product + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit +- + Create a sales order with a line of 100 PCE incoming shipment, with route_id crossdock shipping. +- + !record {model: sale.order, id: sale_order_crossdock_shpng}: + partner_id: base.res_partner_4 + warehouse_id: wh_pps + note: Create Sales order + order_line: + - product_id: cross_shop_product + product_uom_qty: 100.00 - !python {model: sale.order.line}: | - route_warehouse0_crossdock = self.pool.get('stock.warehouse').browse(cr, uid, ref('stock.warehouse0')).crossdock_route_id.id + route_wh_pps_crossdock = self.pool.get('stock.warehouse').browse(cr, uid, ref('stock_dropshipping.wh_pps')).crossdock_route_id.id order = self.pool.get('sale.order').browse(cr, uid, ref('sale_order_crossdock_shpng')) line_ids = [x.id for x in order.order_line] - self.write(cr, uid, line_ids, {'route_id': route_warehouse0_crossdock}) -- - Confirm sales order -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_crossdock_shpng} -- - Check a quotation was created to a certain supplier and confirm so it becomes a confirmed purchase order + self.write(cr, uid, line_ids, {'route_id': route_wh_pps_crossdock}) +- + Confirm sales order. +- + !workflow {model: sale.order, action: order_confirm, ref: sale_order_crossdock_shpng} +- + Run the scheduler. +- + !python {model: procurement.order}: | + self.run_scheduler(cr, uid) +- + Check a quotation was created for the created supplier and confirm it. - !python {model: purchase.order}: | - po_id = self.search(cr, uid, [('partner_id', '=', ref('base.res_partner_2'))]) + po_id = self.search(cr, uid, [('partner_id', '=', ref('supplier_crossdock')), ('state','=','draft')]) self.wkf_confirm_order(cr, uid, po_id) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index e49af16497159d516d20f31c87f6d9d450c7f3ea..b01e8ade9ffae09b28a44dcbbca1d13cedbc5f3b 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -35,6 +35,7 @@ from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound from openerp.modules import get_module_resource from openerp.tools import topological_sort from openerp.tools.translate import _ +from openerp.tools import ustr from openerp import http from openerp.http import request, serialize_exception as _serialize_exception @@ -444,14 +445,14 @@ def xml2json_from_elementtree(el, preserve_whitespaces=False): return res def content_disposition(filename): - filename = filename.encode('utf8') - escaped = urllib2.quote(filename) + filename = ustr(filename) + escaped = urllib2.quote(filename.encode('utf8')) browser = request.httprequest.user_agent.browser version = int((request.httprequest.user_agent.version or '0').split('.')[0]) if browser == 'msie' and version < 9: return "attachment; filename=%s" % escaped elif browser == 'safari': - return "attachment; filename=%s" % filename + return u"attachment; filename=%s" % filename else: return "attachment; filename*=UTF-8''%s" % escaped @@ -1182,6 +1183,7 @@ class Binary(http.Controller): } except Exception: args = {'error': "Something horrible happened"} + _logger.exception("Fail to upload attachment %s" % ufile.filename) return out % (simplejson.dumps(callback), simplejson.dumps(args)) @http.route([ @@ -1440,6 +1442,9 @@ class ExportFormat(object): context = dict(request.context or {}, **params.get('context', {})) ids = ids or Model.search(domain, 0, False, False, context) + if not request.env[model]._is_an_ordinary_table(): + fields = [field for field in fields if field['name'] != 'id'] + field_names = map(operator.itemgetter('name'), fields) import_data = Model.export_data(ids, field_names, self.raw_data, context=context).get('datas',[]) diff --git a/addons/web/static/lib/datejs/globalization/bs-BA.js b/addons/web/static/lib/datejs/globalization/bs-BA.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/addons/web/static/lib/datejs/globalization/kab_DZ.js b/addons/web/static/lib/datejs/globalization/kab_DZ.js new file mode 100644 index 0000000000000000000000000000000000000000..7cc18e666afd9e8b618db0290183f00143536511 --- /dev/null +++ b/addons/web/static/lib/datejs/globalization/kab_DZ.js @@ -0,0 +1,194 @@ +Date.CultureInfo = { + /* Culture Name */ + name: "kab", + englishName: "Kabyle (Algaria)", + nativeName: "Taqbaylit (Ledzayer)", + + /* Day Name Strings */ + dayNames: ["acer", "arim", "aram", "ahad", "amhad", "sem", "sed"], + abbreviatedDayNames: ["ace.", "ari.", "ara.", "aha.", "amh.", "sem.", "sed."], + shortestDayNames: ["ac", "ar", "ra", "ah", "am", "sm", "sd"], + firstLetterDayNames: ["c", "r", "m", "h", "a", "s", "d"], + + /* Month Name Strings */ + monthNames: ["yennayer", "furar", "meÉ£res", "yebrir", "maggu", "yunyu", "yulyu", "É£uct", "ctember", "tuber", "number", "bujember"], + abbreviatedMonthNames: ["yen.", "fur.", "meÉ£.", "yeb.", "mag.", "yun.", "yul.", "É£uc", "cte.", "tub.", "num.", "buj."], + /* AM/PM Designators */ + amDesignator: "", + pmDesignator: "", + + firstDayOfWeek: 1, + twoDigitYearMax: 2029, + + /** + * The dateElementOrder is based on the order of the + * format specifiers in the formatPatterns.DatePattern. + * + * Example: + <pre> + shortDatePattern dateElementOrder + ------------------ ---------------- + "M/d/yyyy" "mdy" + "dd/MM/yyyy" "dmy" + "yyyy-MM-dd" "ymd" + </pre> + * + * The correct dateElementOrder is required by the parser to + * determine the expected order of the date elements in the + * string being parsed. + */ + dateElementOrder: "dmy", + + /* Standard date and time format patterns */ + formatPatterns: { + shortDate: "dd/MM/yyyy", + longDate: "dddd d MMMM yyyy", + shortTime: "HH:mm", + longTime: "HH:mm:ss", + fullDateTime: "dddd d MMMM yyyy HH:mm:ss", + sortableDateTime: "yyyy-MM-ddTHH:mm:ss", + universalSortableDateTime: "yyyy-MM-dd HH:mm:ssZ", + rfc1123: "ddd, dd MMM yyyy HH:mm:ss GMT", + monthDay: "d MMMM", + yearMonth: "MMMM yyyy" + }, + + /** + * NOTE: If a string format is not parsing correctly, but + * you would expect it parse, the problem likely lies below. + * + * The following regex patterns control most of the string matching + * within the parser. + * + * The Month name and Day name patterns were automatically generated + * and in general should be (mostly) correct. + * + * Beyond the month and day name patterns are natural language strings. + * Example: "next", "today", "months" + * + * These natural language string may NOT be correct for this culture. + * If they are not correct, please translate and edit this file + * providing the correct regular expression pattern. + * + * If you modify this file, please post your revised CultureInfo file + * to the Datejs Forum located at http://www.datejs.com/forums/. + * + * Please mark the subject of the post with [CultureInfo]. Example: + * Subject: [CultureInfo] Translated "da-DK" Danish(Denmark) + * + * We will add the modified patterns to the master source files. + * + * As well, please review the list of "Future Strings" section below. + */ + regexPatterns: { + jan: /^yenna(.(yer)?)?/i, + feb: /^fur(.(ar)?)?/i, + mar: /^meÉ£(.(res)?)?/i, + apr: /^yeb(.(rir)?)?/i, + may: /^mag(.(gu)?)?/i, + jun: /^yun(.(yu)?)?/i, + jul: /^yul(.(yu)?)?/i, + aug: /^É£uct/i, + sep: /^ctem(.(ber)?)?/i, + oct: /^tu(.(ber)?)?/i, + nov: /^num(.(ber)?)?/i, + dec: /^bujem(.(ber)?)?/i, + + sun: /^ac(.(er)?)?/i, + mon: /^ar(.(im)?)?/i, + tue: /^ar(.(am)?)?/i, + wed: /^ah(.(ad)?)?/i, + thu: /^am(.(had)?)?/i, + fri: /^se(.(m)?)?/i, + sat: /^se(.(d)?)?/i, + + future: /^next/i, + past: /^last|past|prev(ious)?/i, + add: /^(\+|aft(er)?|from|hence)/i, + subtract: /^(\-|bef(ore)?|ago)/i, + + yesterday: /^yes(terday)?/i, + today: /^t(od(ay)?)?/i, + tomorrow: /^tom(orrow)?/i, + now: /^n(ow)?/i, + + millisecond: /^ms|milli(second)?s?/i, + second: /^sec(ond)?s?/i, + minute: /^mn|min(ute)?s?/i, + hour: /^h(our)?s?/i, + week: /^w(eek)?s?/i, + month: /^m(onth)?s?/i, + day: /^d(ay)?s?/i, + year: /^y(ear)?s?/i, + + shortMeridian: /^(a|p)/i, + longMeridian: /^(a\.?m?\.?|p\.?m?\.?)/i, + timezone: /^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt|utc)/i, + ordinalSuffix: /^\s*(st|nd|rd|th)/i, + timeContext: /^\s*(\:|a(?!u|p)|p)/i + }, + + timezones: [{name:"UTC", offset:"-000"}, {name:"GMT", offset:"-000"}, {name:"EST", offset:"-0500"}, {name:"EDT", offset:"-0400"}, {name:"CST", offset:"-0600"}, {name:"CDT", offset:"-0500"}, {name:"MST", offset:"-0700"}, {name:"MDT", offset:"-0600"}, {name:"PST", offset:"-0800"}, {name:"PDT", offset:"-0700"}] +}; + +/******************** + ** Future Strings ** + ******************** + * + * The following list of strings may not be currently being used, but + * may be incorporated into the Datejs library later. + * + * We would appreciate any help translating the strings below. + * + * If you modify this file, please post your revised CultureInfo file + * to the Datejs Forum located at http://www.datejs.com/forums/. + * + * Please mark the subject of the post with [CultureInfo]. Example: + * Subject: [CultureInfo] Translated "da-DK" Danish(Denmark)b + * + * English Name Translated + * ------------------ ----------------- + * about about + * ago ago + * date date + * time time + * calendar calendar + * show show + * hourly hourly + * daily daily + * weekly weekly + * bi-weekly bi-weekly + * fortnight fortnight + * monthly monthly + * bi-monthly bi-monthly + * quarter quarter + * quarterly quarterly + * yearly yearly + * annual annual + * annually annually + * annum annum + * again again + * between between + * after after + * from now from now + * repeat repeat + * times times + * per per + * min (abbrev minute) min + * morning morning + * noon noon + * night night + * midnight midnight + * mid-night mid-night + * evening evening + * final final + * future future + * spring spring + * summer summer + * fall fall + * winter winter + * end of end of + * end end + * long long + * short short + */ diff --git a/addons/web/static/lib/datejs/globalization/lo-LA.js b/addons/web/static/lib/datejs/globalization/lo-LA.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 805790930e2cfcd0656284a9c5e892502536787c..81a1460b0bd6603a680d8947100c54020a93e1e3 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -52,6 +52,7 @@ box-shadow: 0 0 1px black; left: 50%; transform: translate(-50%, 0); + -webkit-filter: blur(0); } .oe_notification { @@ -3048,6 +3049,12 @@ body.oe_single_form .oe_single_form_container { @media print { body { height: auto !important; + overflow: visible !important; + } + + tr, td { + page-break-inside: avoid; + page-break-after: auto; } .openerp#announcement_bar_table { @@ -3057,6 +3064,13 @@ body.oe_single_form .oe_single_form_container { .openerp { text-shadow: none; } + .openerp.openerp_webclient_container { + display: block; + height: auto !important; + } + .openerp .oe-view-manager { + display: block; + } .openerp .oe_application, .openerp .oe_view_manager_wrapper { height: auto !important; } @@ -3082,6 +3096,9 @@ body.oe_single_form .oe_single_form_container { .openerp .oe_notebook > li.ui-tabs-selected { display: block; } + .openerp .oe_application { + width: auto; + } .openerp .oe_application .oe_form_sheet, .openerp .oe_application .oe_form_sheetbg { border: 0px !important; box-shadow: 0px 0px 0px; @@ -3099,6 +3116,9 @@ body.oe_single_form .oe_single_form_container { .openerp .openerp div.oe_mail_wall { overflow: hidden !important; } + .openerp .oe-view-manager .oe-view-manager-content { + overflow: visible; + } .openerp.openerp_webclient_container { overflow: visible; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index d848bbe6fc0410e40940e39d0c2a3cc6b3e9aabe..1f7a0c1560f2a6d25b289388e8be6a9cf441771c 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -161,6 +161,7 @@ $sheet-padding: 16px box-shadow: 0 0 1px black left: 50% transform: translate(-50%, 0) + -webkit-filter: blur(0) .oe_notification z-index: 1600 @@ -2520,9 +2521,18 @@ body.oe_single_form @media print body height: auto !important + overflow: visible !important + tr, td + page-break-inside: avoid + page-break-after: auto .openerp#announcement_bar_table display: none .openerp + &.openerp_webclient_container + display: block + height: auto !important + .oe-view-manager + display: block .oe_application, .oe_view_manager_wrapper height: auto !important .oe_application > div > .oe_view_manager > .oe_view_manager_wrapper > div > .oe_view_manager_body @@ -2550,6 +2560,7 @@ body.oe_single_form box-shadow: 0px 0px 0px .oe_list overflow-x: visible + width: auto .oe-control-panel, .oe-o2m-control-panel border: 0px !important box-shadow: 0px 0px 0px @@ -2558,6 +2569,8 @@ body.oe_single_form background: none .openerp div.oe_mail_wall overflow: hidden !important + .oe-view-manager .oe-view-manager-content + overflow: visible .openerp.openerp_webclient_container overflow: visible // }}} diff --git a/addons/web/static/src/js/action_manager.js b/addons/web/static/src/js/action_manager.js index 7b9cfa9cbf88588557ccb2251754113dc1b23532..6700932b4bc9016cfaa80ea4eb7ede950c61796e 100644 --- a/addons/web/static/src/js/action_manager.js +++ b/addons/web/static/src/js/action_manager.js @@ -507,7 +507,8 @@ var ActionManager = Widget.extend({ pager : (!popup || !form) && !inline, display_title : !popup, headless: (popup || inline) && form, - search_disable_custom_filters: action.context && action.context.search_disable_custom_filters + search_disable_custom_filters: action.context && action.context.search_disable_custom_filters, + default_view: action.context && action.context.params && action.context.params.view_type, }); } diff --git a/addons/web/static/src/js/framework/crash_manager.js b/addons/web/static/src/js/framework/crash_manager.js index 01fef03267baed2def171bad4516706fdfe9282f..303cd66a4a3b8587583bf7dcbc586c10e73f5fd5 100644 --- a/addons/web/static/src/js/framework/crash_manager.js +++ b/addons/web/static/src/js/framework/crash_manager.js @@ -36,19 +36,24 @@ var CrashManager = core.Class.extend({ this.active = false; }, rpc_error: function(error) { + var self = this; if (!this.active) { return; } + if (this.$indicator){ + return; + } if (error.code == -32098) { $.blockUI({ message: '' , overlayCSS: {'z-index': 9999, backgroundColor: '#FFFFFF', opacity: 0.0, cursor: 'wait'}}); - var $indicator = $('<div class="oe_indicator">' + _t("Trying to reconnect... ") + '<i class="fa fa-refresh"></i></div>'); - $indicator.prependTo("body"); + this.$indicator = $('<div class="oe_indicator">' + _t("Trying to reconnect... ") + '<i class="fa fa-refresh"></i></div>'); + this.$indicator.prependTo("body"); var timeinterval = setInterval(function(){ ajax.jsonRpc('/web/webclient/version_info').then(function() { clearInterval(timeinterval); - $indicator.html(_t("You are back online")); - $indicator.delay(2000).fadeOut('slow',function(){ - $indicator.remove(); + self.$indicator.html(_t("You are back online")); + self.$indicator.delay(2000).fadeOut('slow',function(){ + $(this).remove(); + self.$indicator.remove(); }); $.unblockUI(); }); diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js index ea64a55f22cffaa43a05c73883c64a9817ea040d..234b681f7d2cfde108c3ef43672d610daab35a20 100644 --- a/addons/web/static/src/js/tour.js +++ b/addons/web/static/src/js/tour.js @@ -76,6 +76,7 @@ var localStorage = window.localStorage; var Tour = { tours: {}, defaultDelay: 50, + autoRunning: true, retryRunningDelay: 1000, errorDelay: 5000, state: null, @@ -378,6 +379,7 @@ var Tour = { }, logError: function (step, message, all) { var state = Tour.getState(); + console.log(state.tour.steps.slice()); message += '\ntour: ' + state.id + (step ? '\nstep: ' + step.id + ": '" + (step._title || step.title) + "'" : '' ) + (all ? '\nhref: ' + window.location.href : '' ) @@ -711,7 +713,11 @@ var Tour = { ///////////////////////////////////////////////// -$(document).ready(Tour.running); +$(document).ready(function () { + if (Tour.autoRunning) { + Tour.running(); + }; +}); return Tour; diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index aa06074401f7fc4bdb763dbae39532fd49bacb32..df570967f94258d15f286a693d7e2a8cbb319420 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -637,7 +637,7 @@ var FormView = View.extend(common.FieldManagerMixin, { // If field is not defined in the view, just ignore it if (field) { var value_ = values[f]; - if (field.get_value() != value_) { + if (field.get_value() !== value_) { field._inhibit_on_change_flag = true; field.set_value(value_); field._inhibit_on_change_flag = false; diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index b23bff4cc846f3f541d836c391b766951b7a85a6..ee04f8376b4fb9e76a4f3ddba0bc235b41abc841 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -1230,9 +1230,11 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { set_filename: function(value) { var filename = this.node.attrs.filename; if (filename) { - var tmp = {}; - tmp[filename] = value; - this.field_manager.set_values(tmp); + var field = this.field_manager.fields[filename]; + if (field) { + field.set_value(value); + field._dirty_flag = true; + } } }, on_clear: function() { @@ -1279,10 +1281,10 @@ var FieldBinaryFile = FieldBinary.extend({ }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { this.binary_value = true; + this.set_filename(name); this.internal_set_value(file_base64); var show_value = name + " (" + utils.human_size(size) + ")"; this.$el.find('input').eq(0).val(show_value); - this.set_filename(name); }, on_clear: function() { this._super.apply(this, arguments); diff --git a/addons/web/static/src/js/views/graph_widget.js b/addons/web/static/src/js/views/graph_widget.js index 1e712b5af790c6e21e8c4318a49edc1bceaff94a..3f8a7566d9786c2bb51b3fb8e3a33cea480b8afe 100644 --- a/addons/web/static/src/js/views/graph_widget.js +++ b/addons/web/static/src/js/views/graph_widget.js @@ -168,6 +168,7 @@ return Widget.extend({ // rotateLabels: 40, showControls: (this.groupbys.length > 1) }); + chart.yAxis.tickFormat(function(d) { return openerp.web.format_value(d, { type : 'float' });}); chart(svg); this.to_remove = chart.update; diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index 1b55895d84752dc60b703fada0a143268c65438b..22b74ecbf15717e2ea4ff78321f328aa6dbb4350 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -60,6 +60,8 @@ var PivotView = View.extend({ this.last_header_selected = null; this.sorted_column = {}; + + this.numbering = {}; }, willStart: function () { var self = this; @@ -490,13 +492,22 @@ var PivotView = View.extend({ }, sanitize_value: function (value, field) { if (value === false) return _t("Undefined"); - if (value instanceof Array) return value[1]; + if (value instanceof Array) return this.get_numbered_value(value, field); if (field && this.fields[field] && (this.fields[field].type === 'selection')) { var selected = _.where(this.fields[field].selection, {0: value})[0]; return selected ? selected[1] : value; } return value; }, + get_numbered_value: function(value, field) { + var id= value[0]; + var name= value[1] + this.numbering[field] = this.numbering[field] || {}; + this.numbering[field][name] = this.numbering[field][name] || {}; + var numbers = this.numbering[field][name]; + numbers[id] = numbers[id] || _.size(numbers) + 1; + return name + (numbers[id] > 1 ? " (" + numbers[id] + ")" : ""); + }, make_header: function (data_pt, root, i, j, parent_header) { var attrs = data_pt.attributes, value = attrs.value, diff --git a/addons/web/static/src/js/views/search_filters.js b/addons/web/static/src/js/views/search_filters.js index 0409fa90a0dcfed5f6cbd49c504814361a346c1c..506ddecd8055eb8491060ffac361839b7d445be9 100644 --- a/addons/web/static/src/js/views/search_filters.js +++ b/addons/web/static/src/js/views/search_filters.js @@ -244,6 +244,10 @@ ExtendedSearchProposition.Float = ExtendedSearchProposition.Field.extend({ {value: "∃", text: _lt("is set")}, {value: "∄", text: _lt("is not set")} ], + init: function (parent) { + this._super(parent); + this.decimal_point = _t.database.parameters.decimal_point; + }, toString: function () { return this.$el.val(); }, diff --git a/addons/web/static/src/js/views/search_inputs.js b/addons/web/static/src/js/views/search_inputs.js index 16f95340dd6f7cb5c08a7ffd456ee0068fec9514..4127b0209707d021397293bd5ea632b842a99040 100644 --- a/addons/web/static/src/js/views/search_inputs.js +++ b/addons/web/static/src/js/views/search_inputs.js @@ -22,6 +22,7 @@ var Input = Widget.extend( /** @lends instance.web.search.Input# */{ */ init: function (parent) { this._super(parent); + this.searchview = parent; this.load_attrs({}); }, /** @@ -406,9 +407,17 @@ var ManyToOneField = CharField.extend({ // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) var context = pyeval.eval( 'contexts', [this.searchview.dataset.get_context()]); + var args = this.attrs.domain; + if (typeof args === 'string') { + try { + args = pyeval.eval('domain', args); + } catch(e) { + args = []; + } + } return this.model.call('name_search', [], { name: needle, - args: (typeof this.attrs.domain === 'string') ? [] : this.attrs.domain, + args: args, limit: 8, context: context }).then(function (results) { @@ -433,7 +442,8 @@ var ManyToOneField = CharField.extend({ // to handle this as if it were a single value. value = value[0]; } - return this.model.call('name_get', [value]).then(function (names) { + var context = pyeval.eval('contexts', [this.searchview.dataset.get_context()]); + return this.model.call('name_get', [value], {context: context}).then(function (names) { if (_(names).isEmpty()) { return null; } return facet_from(self, names[0]); }); diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js index 1dd1b217951748e2ee3b6016a270a947a9ccb576..3511bbca01c74de77b10853381669a2fc368578f 100644 --- a/addons/web/static/src/js/views/search_view.js +++ b/addons/web/static/src/js/views/search_view.js @@ -888,14 +888,15 @@ return Widget.extend({ }, expand: function () { var self = this; - this.current_result.expand(this.get_search_string()).then(function (results) { + var current_result = this.current_result; + current_result.expand(this.get_search_string()).then(function (results) { (results || [{label: '(no result)'}]).reverse().forEach(function (result) { result.indent = true; var $li = self.make_list_item(result); - self.current_result.$el.after($li); + current_result.$el.after($li); }); - self.current_result.expanded = true; - self.current_result.$el.find('span.oe-expand').html('â–¼'); + current_result.expanded = true; + current_result.$el.find('span.oe-expand').html('â–¼'); }); }, fold: function () { diff --git a/addons/web/static/src/js/widgets/sidebar.js b/addons/web/static/src/js/widgets/sidebar.js index e0bdcd3f57cf4f8f9a2dd65132a7eb3f75c2345a..5dc6c147f39e6c27af4259623bbf37d63e6eadcf 100644 --- a/addons/web/static/src/js/widgets/sidebar.js +++ b/addons/web/static/src/js/widgets/sidebar.js @@ -16,6 +16,7 @@ var Sidebar = Widget.extend({ init: function(parent) { var self = this; this._super(parent); + this.view = this.getParent(); this.sections = [ { 'name' : 'print', 'label' : _t('Print'), }, { 'name' : 'other', 'label' : _t('More'), } diff --git a/addons/web/static/src/js/widgets/user_menu.js b/addons/web/static/src/js/widgets/user_menu.js index fa5ea2bbdf731ab97743ddc75d88f04ae9298390..dba1704c71995c68c767bd3b11e5fafd26ec10ba 100644 --- a/addons/web/static/src/js/widgets/user_menu.js +++ b/addons/web/static/src/js/widgets/user_menu.js @@ -87,7 +87,7 @@ var SystrayMenu = Widget.extend({ framework.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params)); }).fail(function(result, ev){ ev.preventDefault(); - framework.redirect('https://accounts.odoo.com/web'); + framework.redirect('https://accounts.odoo.com/account'); }); } }, diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 7bc79aec9eabaa2e7a0dd2cca8fb1288b8b800aa..f8f3c7191f3902aac2dd92f98473a877a1f7d9ab 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -609,12 +609,12 @@ </t> <a class="oe_file_attachment" t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"> <t t-raw="item.label"/> - <span t-if="section.name == 'files' and !item.callback" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment"> + <span t-if="widget.view.is_action_enabled('edit') && section.name == 'files' and !item.callback" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment"> <i class="fa fa-trash-o"/> </span> </a> </li> - <li t-if="section.name == 'files'" class="oe_sidebar_add_attachment"> + <li t-if="widget.view.is_action_enabled('edit') && section.name == 'files'" class="oe_sidebar_add_attachment"> <t t-call="HiddenInputFile"> <t t-set="fileupload_id" t-value="widget.fileupload_id"/> <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t> @@ -1755,7 +1755,11 @@ <input type="number" class="field_integer form-control" value = "0" step="1"/> </t> <t t-name="SearchView.extended_search.proposition.float"> - <input type="number" class="field_float form-control" value = "0.0" step="0.01"/> + <input class="field_float form-control" + t-att-type="widget.decimal_point === '.' ? 'number' : 'text'" + t-attf-title="Number using #{widget.decimal_point || '.' } as decimal separator." + t-attf-pattern="[0-9]+([\\#{widget.decimal_point || '.' }][0-9]+)?" + t-attf-value="0#{widget.decimal_point || '.' }0" step="0.01"/> </t> <t t-name="SearchView.extended_search.proposition.selection"> <select class="form-control"> diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 84c6d0d3668bfdc654280770e645243c17106c9b..e1c6368aa099780b7175b362d4baee6ff0015303 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -339,7 +339,7 @@ odoo.define_section('search.defaults', ['web.search_inputs', 'web.SearchView', ' var ManyToOneField = core.search_widgets_registry.get('many2one'); var Facet = SearchView.Facet; - var view = {inputs: []}, id = 4; + var view = {inputs: [], dataset: {get_context: function () {}}}, id = 4; var f = new ManyToOneField( {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, @@ -375,7 +375,7 @@ odoo.define_section('search.defaults', ['web.search_inputs', 'web.SearchView', ' var ManyToOneField = core.search_widgets_registry.get('many2one'); var Facet = SearchView.Facet; - var view = {inputs: []}, id = 5; + var view = {inputs: [], dataset: {get_context: function () {}}}, id = 5; var f = new ManyToOneField( {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, @@ -402,7 +402,7 @@ odoo.define_section('search.defaults', ['web.search_inputs', 'web.SearchView', ' var ManyToOneField = core.search_widgets_registry.get('many2one'); - var view = {inputs: []}, id = 4; + var view = {inputs: [], dataset: {get_context: function () {}}}, id = 4; var f = new ManyToOneField( {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, diff --git a/addons/web_diagram/controllers/main.py b/addons/web_diagram/controllers/main.py index 0e89eb13db24c6f0b6d9eaf8b6a97df8410512b4..c45790c7264310174faea551a6e7bf9d50d7839a 100644 --- a/addons/web_diagram/controllers/main.py +++ b/addons/web_diagram/controllers/main.py @@ -1,4 +1,5 @@ import openerp +from openerp.tools.safe_eval import safe_eval as eval class DiagramView(openerp.http.Controller): diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index bbfc22f61638aaf9c8811c501f296b37c043a770..a488267752a256cf34db3f0633e052d66200ade4 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -355,6 +355,7 @@ var KanbanView = View.extend({ }).done(null, function() { def.reject(); }); + return def; }); return def; }, diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 7eccae13072dd973acc1aedb0f938d30d286ca5d..f300487e7b821f12004bc5c5f2d5920547d63db7 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -70,7 +70,7 @@ class Website(openerp.addons.web.controllers.main.Home): redirect.set_cookie('website_lang', lang) return redirect - @http.route('/page/<page:page>', type='http', auth="public", website=True) + @http.route('/page/<page:page>', type='http', auth="public", website=True, cache=300) def page(self, page, **opt): values = { 'path': page, diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index a5119e40760b91a6bd7d31c20dc3a17485389728..2668e5008d6568bcbf78472c4b4122c1febe36c1 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -2,6 +2,7 @@ import logging import os import re +import time import traceback import werkzeug @@ -15,6 +16,7 @@ from openerp.addons.website.models.website import slug, url_for, _UNSLUG_RE from openerp.http import request from openerp.tools import config from openerp.osv import orm +from openerp.tools.safe_eval import safe_eval as eval logger = logging.getLogger(__name__) @@ -26,7 +28,7 @@ class ir_http(orm.AbstractModel): _inherit = 'ir.http' rerouting_limit = 10 - geo_ip_resolver = None + _geoip_resolver = None def _get_converters(self): return dict( @@ -45,7 +47,7 @@ class ir_http(orm.AbstractModel): else: request.uid = request.session.uid - bots = "bot|crawl|slurp|spider|curl|wget".split("|") + bots = "bot|crawl|slurp|spider|curl|wget|facebookexternalhit".split("|") def is_a_bot(self): # We don't use regexp and ustr voluntarily # timeit has been done to check the optimum method @@ -55,6 +57,39 @@ class ir_http(orm.AbstractModel): except UnicodeDecodeError: return any(bot in ua.encode('ascii', 'ignore') for bot in self.bots) + def get_nearest_lang(self, lang): + # Try to find a similar lang. Eg: fr_BE and fr_FR + if lang in request.website.get_languages(): + return lang + + short = lang.split('_')[0] + for code, name in request.website.get_languages(): + if code.startswith(short): + return code + return False + + def _geoip_setup_resolver(self): + if self._geoip_resolver is None: + try: + import GeoIP + # updated database can be downloaded on MaxMind website + # http://dev.maxmind.com/geoip/legacy/install/city/ + geofile = config.get('geoip_database') + if os.path.exists(geofile): + self._geoip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD) + else: + self._geoip_resolver = False + logger.warning('GeoIP database file %r does not exists, apt-get install geoip-database-contrib or download it from http://dev.maxmind.com/geoip/legacy/install/city/', geofile) + except ImportError: + self._geoip_resolver = False + + def _geoip_resolve(self): + if 'geoip' not in request.session: + record = {} + if self._geoip_resolver and request.httprequest.remote_addr: + record = self._geoip_resolver.record_by_addr(request.httprequest.remote_addr) or {} + request.session['geoip'] = record + def _dispatch(self): first_pass = not hasattr(request, 'website') request.website = None @@ -75,24 +110,8 @@ class ir_http(orm.AbstractModel): func and func.routing.get('multilang', func.routing['type'] == 'http') ) - if 'geoip' not in request.session: - record = {} - if self.geo_ip_resolver is None: - try: - import GeoIP - # updated database can be downloaded on MaxMind website - # http://dev.maxmind.com/geoip/legacy/install/city/ - geofile = config.get('geoip_database') - if os.path.exists(geofile): - self.geo_ip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD) - else: - self.geo_ip_resolver = False - logger.warning('GeoIP database file %r does not exists', geofile) - except ImportError: - self.geo_ip_resolver = False - if self.geo_ip_resolver and request.httprequest.remote_addr: - record = self.geo_ip_resolver.record_by_addr(request.httprequest.remote_addr) or {} - request.session['geoip'] = record + self._geoip_setup_resolver() + self._geoip_resolve() cook_lang = request.httprequest.cookies.get('website_lang') if request.website_enabled: @@ -109,50 +128,64 @@ class ir_http(orm.AbstractModel): request.context['website_id'] = request.website.id langs = [lg[0] for lg in request.website.get_languages()] path = request.httprequest.path.split('/') - if first_pass: - if request.website_multilang: - is_a_bot = self.is_a_bot() - # If the url doesn't contains the lang and that it's the first connection, we to retreive the user preference if it exists. - if not path[1] in langs and not is_a_bot: - request.lang = cook_lang or request.lang - if request.lang not in langs: - # Try to find a similar lang. Eg: fr_BE and fr_FR - short = request.lang.split('_')[0] - langs_withshort = [lg[0] for lg in request.website.get_languages() if lg[0].startswith(short)] - if len(langs_withshort): - request.lang = langs_withshort[0] - else: - request.lang = request.website.default_lang_code - else: - request.lang = request.website.default_lang_code - + nearest_lang = not func and self.get_nearest_lang(path[1]) + url_lang = nearest_lang and path[1] + preferred_lang = ((cook_lang if cook_lang in langs else False) + or self.get_nearest_lang(request.lang) + or request.website.default_lang_code) + + is_a_bot = self.is_a_bot() + + request.lang = request.context['lang'] = nearest_lang or preferred_lang + # if lang in url but not the displayed or default language --> change or remove + # or no lang in url, and lang to dispay not the default language --> add lang + # and not a POST request + # and not a bot or bot but default lang in url + if ((url_lang and (url_lang != request.lang or url_lang == request.website.default_lang_code)) + or (not url_lang and request.website_multilang and request.lang != request.website.default_lang_code) + and request.httprequest.method != 'POST') \ + and (not is_a_bot or (url_lang and url_lang == request.website.default_lang_code)): + if url_lang: + path.pop(1) if request.lang != request.website.default_lang_code: path.insert(1, request.lang) - path = '/'.join(path) or '/' - redirect = request.redirect(path + '?' + request.httprequest.query_string) - redirect.set_cookie('website_lang', request.lang) - return redirect + path = '/'.join(path) or '/' + redirect = request.redirect(path + '?' + request.httprequest.query_string) + redirect.set_cookie('website_lang', request.lang) + return redirect + elif url_lang: + path.pop(1) + return self.reroute('/'.join(path) or '/') - request.context['lang'] = request.lang if not request.context.get('tz'): request.context['tz'] = request.session['geoip'].get('time_zone') - if not func: - if path[1] in langs: - request.lang = request.context['lang'] = path.pop(1) - path = '/'.join(path) or '/' - if request.lang == request.website.default_lang_code: - # If language is in the url and it is the default language, redirect - # to url without language so google doesn't see duplicate content - resp = request.redirect(path + '?' + request.httprequest.query_string, code=301) - if cook_lang != request.lang: # If default lang setted in url directly - resp.set_cookie('website_lang', request.lang) - return resp - return self.reroute(path) # bind modified context request.website = request.website.with_context(request.context) - resp = super(ir_http, self)._dispatch() - if not cook_lang: + + # cache for auth public + cache_time = getattr(func, 'routing', {}).get('cache') + cache_enable = cache_time and request.httprequest.method == "GET" and request.website.user_id.id == request.uid + cache_response = None + if cache_enable: + key = (self._name, "cache", request.uid, request.lang, request.httprequest.full_path) + try: + r = self.pool.cache[key] + if r['time'] + cache_time > time.time(): + cache_response = openerp.http.Response(r['content'], mimetype=r['mimetype']) + else: + del self.pool.cache[key] + except KeyError: + pass + + if cache_response: + request.cache_save = False + resp = cache_response + else: + request.cache_save = key if cache_enable else False + resp = super(ir_http, self)._dispatch() + + if request.website_enabled and cook_lang != request.lang and hasattr(resp, 'set_cookie'): resp.set_cookie('website_lang', request.lang) return resp diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index f8b52978192228d8f5db5cbaabc15758b6912830..c63f67a191ca086835b7fcb6154ea2dac5ee3dd8 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: ascii -*- import copy import logging @@ -43,7 +43,7 @@ class view(osv.osv): # Try to fallback on key instead of xml_id rec_id = self.search(cr, uid, [('key', '=', view_id)], context=context) if rec_id: - _logger.info("Could not find view with `xml_id´ '%s', fallback on `key´" % (view_id)) + _logger.info("Could not find view with `xml_id' '%s', fallback on `key'" % (view_id)) return self.browse(cr, uid, rec_id, context=context)[0] else: raise diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index 6b631348651dd6f65d9866859c69902b90ac2f47..b49c6a14afeed17ac8d4cd296b4bdce1aa14105e 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -636,8 +636,7 @@ ul.oe_menu_editor .disclose { } .oe_ace_view_editor.oe_ace_closed { z-index: -1000; - filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); - opacity: 0; + display: none } .oe_include_bundles { diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index 73c000f737a06d5cfc2e4fb846e48fb0f5a28ef8..23762bb5fe4ac944424b90341191e7fc2bfe4ca0 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -546,7 +546,7 @@ $infobar_height: 20px +opacity(0.97) &.oe_ace_closed z-index: -1000 - +opacity(0) + display: none .oe_include_bundles font-weight: normal diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index a7d05da9eeddea85d2b79e5e7f0673dd892f8e55..872d82b460f13cf024fefe1372bc75c1eed25089 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -213,6 +213,10 @@ li > p { margin: 0; } +font[style*='background'], font[class*='bg-'] { + padding: 0 2px +} + .o_ul_toggle_self, .o_ul_toggle_next { display: none; } @@ -342,8 +346,10 @@ body > nav.navbar { z-index: 10000; } -nav.navbar + #wrapwrap { - padding-top: 34px; +@media (min-width: 767px) { + nav.navbar + #wrapwrap { + padding-top: 34px; + } } /* ----- BOOTSTRAP FIX ----- */ diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index 022b07066adf61622c864a6d5c00671c713e26c2..223bab94a0b04df0fa798bf1f871208f21433feb 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -130,6 +130,9 @@ li > * > li > * > li > * > li > * > li > * > li > ol li > p margin: 0 +font[style*='background'], font[class*='bg-'] + padding: 0 2px + .o_ul_toggle_self, .o_ul_toggle_next display: none .o_ul_folded @@ -238,8 +241,9 @@ body > nav.navbar width: 100% position: absolute z-index: 10000 -nav.navbar + #wrapwrap - padding-top: 34px +@media (max-min: 767px) + nav.navbar + #wrapwrap + padding-top: 34px /* ----- BOOTSTRAP FIX ----- */ diff --git a/addons/website/static/src/img/deers.jpg b/addons/website/static/src/img/deers.jpg index 042f427c0b9a52be4f54a5ed04eb8aa7df432cef..9f4b368a18fdc190c9854b18813f94d06e7d1fa8 100644 Binary files a/addons/website/static/src/img/deers.jpg and b/addons/website/static/src/img/deers.jpg differ diff --git a/addons/website/static/src/img/deers_thumb.jpg b/addons/website/static/src/img/deers_thumb.jpg index 0771162ae49e8e008ef821d6aedc264b74604e11..4a955f5c892ac1207c7ea7b58f0db0ffa88d2fb4 100644 Binary files a/addons/website/static/src/img/deers_thumb.jpg and b/addons/website/static/src/img/deers_thumb.jpg differ diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 9cc84746454fa720d9cd5bf912163f0f5dd11301..5e12647a63af40b8a1c0d7a6d669fc28a28653bc 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -937,6 +937,7 @@ function change_default_bootstrap_animation_to_edit() { website.no_editor = !!$(document.documentElement).data('editable-no-editor'); + website.add_template_file('/website/static/src/xml/website.xml'); website.add_template_file('/website/static/src/xml/website.editor.xml'); website.dom_ready.done(function () { website.ready().then(init_editor); @@ -965,6 +966,16 @@ website.dom_ready.done(function () { }); }); + website.error = function(data, url) { + var $error = $(openerp.qweb.render('website.error_dialog', { + 'title': data.data ? data.data.arguments[0] : "", + 'message': data.data ? data.data.arguments[1] : data.statusText, + 'backend_url': url + })); + $error.appendTo("body"); + $error.modal('show'); + }; + /* ----- TOP EDITOR BAR FOR ADMIN ---- */ var EditorBar = Widget.extend({ @@ -1411,6 +1422,12 @@ var Dialog = Widget.extend({ }, close: function () { this.$el.modal('hide'); + }, + destroy: function () { + this.$el.modal('hide').remove(); + if($(".modal.in").length>0){ + $('body').addClass('modal-open'); + } }, }); diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 3212bf824a1ea565121f4258042684ec4274d638..f0876b44747235e9f66bb7354bee837d2993cc12 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -4,6 +4,7 @@ odoo.define('website.website', function (require) { var ajax = require('web.ajax'); var core = require('web.core'); var session = require('web.session'); +var Tour = require('web.Tour'); var _t = core._t; @@ -169,16 +170,6 @@ function prompt(options, qweb) { return def; } -function error(data, url) { - var $error = $(core.qweb.render('website.error_dialog', { - 'title': data.data ? data.data.arguments[0] : "", - 'message': data.data ? data.data.arguments[1] : data.statusText, - 'backend_url': url - })); - $error.appendTo("body"); - $error.modal('show'); -} - function form(url, method, params) { var htmlform = document.createElement('form'); htmlform.setAttribute('action', url); @@ -270,8 +261,6 @@ function add_template_file(template) { return def; } -add_template_file('/website/static/src/xml/website.xml'); - var dom_ready = $.Deferred(); $(document).ready(function () { dom_ready.resolve(); @@ -402,6 +391,11 @@ dom_ready.then(function () { $('#oe_applications').before($collapse); $collapse.wrap('<div class="visible-xs"/>'); $('[data-target="#oe_applications"]').attr("data-target", "#oe_applications_collapse"); + }); + + Tour.autoRunning = false; + ready().then(function () { + setTimeout(Tour.running,0); }); return { @@ -412,7 +406,6 @@ return { parseHash: parseHash, reload: reload, prompt: prompt, - error: error, form: form, init_kanban: init_kanban, add_template_file: add_template_file, @@ -433,4 +426,4 @@ var Session = require('web.Session'); return new Session(null, null, {modules: ['website']}); -}); \ No newline at end of file +}); diff --git a/addons/website/static/src/js/website.seo.js b/addons/website/static/src/js/website.seo.js index 5a8481574934027946397cd400a66d1ef0114f2b..31a8ebd3e92efa2cfb621d13d4cefbbad0343ca3 100644 --- a/addons/website/static/src/js/website.seo.js +++ b/addons/website/static/src/js/website.seo.js @@ -499,7 +499,7 @@ var Configurator = Widget.extend({ descriptionChanged: function () { var self = this; setTimeout(function () { - var description = self.$('textarea[name=seo_page_description]').attr('value'); + var description = self.$('textarea[name=seo_page_description]').val(); self.htmlPage.changeDescription(description); self.renderPreview(); }, 0); diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index 8d5c98111b381ea0948c988d52e06f3129eea0ea..0b1c7befaa12bb6c605cc2faf8a24c173dc4f777 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -168,7 +168,8 @@ animationRegistry.share = Animation.extend({ var title = encodeURIComponent($("title").text()); this.$("a").each(function () { var $a = $(this); - $a.attr("href", $(this).attr("href").replace("{url}", url).replace("{title}", title)); + var url_regex = /\{url\}|%7Burl%7D/, title_regex = /\{title\}|%7Btitle%7D/; + $a.attr("href", $(this).attr("href").replace(url_regex, url).replace(title_regex, title)); if ($a.attr("target") && $a.attr("target").match(/_blank/i) && !$a.closest('.o_editable').length) { $a.on('click', function () { window.open(this.href,'','menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600'); diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js index 050a649715aaf032396fcfb0159f230e13ba325a..775012e1aa8d4fe1efc4a8b722739730cdd99cbc 100644 --- a/addons/website/static/src/js/website.translator.js +++ b/addons/website/static/src/js/website.translator.js @@ -67,7 +67,10 @@ editor.EditorBar.include({ var link = $('.js_language_selector a[data-default-lang]')[0]; if (link) { link.search += (link.search ? '&' : '?') + 'enable_editor=1'; - window.location = link.attributes.href.value; + var url = link.pathname + link.search + window.location.hash; + link.pathname = '/website/lang/default'; + link.search = '?' + $.param({r: url}); + window.location = link.href; } }, translate: function () { @@ -89,7 +92,8 @@ editor.EditorBar.include({ var $editables = $('[data-oe-model="ir.ui.view"]') .not('link, script') .not('.oe_snippets,.oe_snippet, .oe_snippet *, .navbar-toggle') - .not('[data-oe-type]'); + .not('[data-oe-type]') + .add($('[data-oe-translate="1"]:not([data-oe-model="ir.ui.view"])').addClass('o_editable')); $editables.each(function () { var $node = $(this); @@ -112,7 +116,7 @@ editor.EditorBar.include({ self.sanitizeNode($node[0]); } if (self.getInitialContent($node[0]) !== $node.text()) { - $node.addClass('oe_dirty').removeClass('oe_translatable_todo oe_translatable_inprogress'); + $node.addClass('o_dirty').removeClass('oe_translatable_todo oe_translatable_inprogress'); } }, 0); }); @@ -133,7 +137,7 @@ editor.EditorBar.include({ // TODO: link nodes with same content node.className += ' oe_translatable_text'; node.setAttribute('data-oe-translation-view-id', view_id); - var content = node.childNodes[0].data.trim(); + var content = $(node).text().trim(); var trans = this.translations.filter(function (t) { return t.res_id === view_id && t.value.trim() === content; }); @@ -152,10 +156,9 @@ editor.EditorBar.include({ }, save: function () { var self = this; - var mysuper = this._super; var trans = {}; // this._super.apply(this, arguments); - $('.oe_translatable_text.oe_dirty').each(function () { + $('.oe_translatable_text.o_dirty').each(function () { var $node = $(this); var data = $node.data(); if (!trans[data.oeTranslationViewId]) { @@ -171,8 +174,7 @@ editor.EditorBar.include({ 'data': trans, 'lang': website.get_context()['lang'], }).then(function () { - window.onbeforeunload = null; - website.reload(); + self._save(); }).fail(function () { // TODO: bootstrap alert with error message alert("Could not save translation"); @@ -185,6 +187,7 @@ editor.EditorBar.include({ if (node.attributes['data-oe-translate'].value == '1') { node.className += ' oe_translatable_field'; } + this.markTranslatableNode(node, view_id); } else if (node.childNodes.length === 1 && this.isTextNode(node.childNodes[0]) && !node.getAttribute('data-oe-model')) { diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 7d0916044b9ef20fd558af1ace663d75ac2405cc..aa7d2e8f92b1c114367c9465a46f031d818e1523 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -91,8 +91,8 @@ <t t-set="languages" t-value="website.get_languages() if website else None"/> <t t-if="request and request.website_multilang and website"> - <t t-foreach="website.get_alternate_languages(request.httprequest)" t-as="lang"> - <link rel="alternate" t-att-hreflang="lang['hreflang']" t-att-href="lang['href']"/> + <t t-foreach="website.get_alternate_languages(request.httprequest)" t-as="lg"> + <link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/> </t> </t> @@ -511,7 +511,7 @@ <template id="publish_short"> <t groups="base.group_website_publisher" t-ignore="true"> - <div t-attf-class="btn-group pull-right js_publish_management #{object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="object.id" t-att-data-object="object._name" t-att-data-controller="publish_controller"> + <div t-attf-class="pull-right js_publish_management #{object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="object.id" t-att-data-object="object._name" t-att-data-controller="publish_controller"> <button class="btn btn-danger js_publish_btn">Not Published</button> <button class="btn btn-success js_publish_btn">Published</button> </div> diff --git a/addons/website_crm_score/controllers/main.py b/addons/website_crm_score/controllers/main.py index bbb2c6a7c16130228e877eb273189e4f19677f86..3707a8f648fbed65069cd628ca2c5b04c0e7e9c7 100644 --- a/addons/website_crm_score/controllers/main.py +++ b/addons/website_crm_score/controllers/main.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -from openerp import addons, http, SUPERUSER_ID, fields +from openerp import http, SUPERUSER_ID, fields from openerp.http import request from openerp.tools import html_escape +from openerp.addons.website.controllers.main import Website +from openerp.addons.website_crm.controllers.main import contactus - -class PageController(addons.website.controllers.main.Website): +class PageController(Website): @http.route('/page/<page:page>', auth="public", website=True) def page(self, page, **opt): @@ -32,7 +33,7 @@ class PageController(addons.website.controllers.main.Website): return response -class ContactController(addons.website_crm.controllers.main.contactus): +class ContactController(contactus): @http.route(['/crm/contactus'], type='http', auth="public", website=True) def contactus(self, **kwargs): diff --git a/addons/website_crm_score/static/src/js/display_filters.js b/addons/website_crm_score/static/src/js/display_filters.js index d7817aa4dcfb8f7f92668e32aed24a12bbfc5e7a..859ec6fd21883f467f981c8106d2a55ad8ac222b 100644 --- a/addons/website_crm_score/static/src/js/display_filters.js +++ b/addons/website_crm_score/static/src/js/display_filters.js @@ -22,14 +22,25 @@ var Filters = common.AbstractField.extend({ var val = this.field.raw_value; var self = this; if (val) { - val = eval(val); + // This widget is temporary + // To keep only while the widget domain filter doesn't exist ! + + // Ugly hack to have (more) python domains which can be evaluated in JS + val = val.replace('(', '[').replace(')', ']').replace('False', 'false').replace('True', 'true') + try { + val = eval(val); + } + catch(err) { + // don't block UI if domain is not evaluable in JS + console.debug(err.message); + val = [['error','=', err.message]]; + } if (val.length <= this.MAX_LEN) { var i = 0; while (i < val.length) { var res = this.interpret(val, i); i = res[0]; var $span = res[1]; - // var $span = '<h2>' + res[1] + '</h2>'; self.$el.append($span); } } diff --git a/addons/website_crm_score/static/src/xml/track_page.xml b/addons/website_crm_score/static/src/xml/track_page.xml index 9aaeb8136e19ed4c9ada60bfa8855b10a408a840..18d67a6251e5eba1dc7544116478c52c1ca28fc2 100644 --- a/addons/website_crm_score/static/src/xml/track_page.xml +++ b/addons/website_crm_score/static/src/xml/track_page.xml @@ -4,11 +4,13 @@ <t t-extend="website.seo_configuration"> <t t-jquery="div.modal-body" t-operation="append"> - <section class="set_score"> - <h3 class="track-page">4. Track Page <small>to better score your leads: </small> - <!-- checkbox is added in js in set_score.js --> - </h3> - </section> + <t t-if="widget.getMainObject() && widget.getMainObject().model === 'ir.ui.view'"> + <section class="set_score"> + <h3 class="track-page">4. Track Page <small>to better score your leads: </small> + <!-- checkbox is added in js in set_score.js --> + </h3> + </section> + </t> </t> </t> diff --git a/addons/website_event_sale/controllers/main.py b/addons/website_event_sale/controllers/main.py index 8a5275c9afb7c7b25ce7997f60dffb91633e3e76..d3cedeb272cc21fef182d8bbb8d48df7acb82091 100644 --- a/addons/website_event_sale/controllers/main.py +++ b/addons/website_event_sale/controllers/main.py @@ -42,7 +42,7 @@ class website_event(website_event): def _process_tickets_details(self, data): ticket_post = {} for key, value in data.iteritems(): - if not key.startswith('nb_register') or not '-' in key: + if not key.startswith('nb_register') or '-' not in key: continue items = key.split('-') if len(items) < 2: @@ -61,7 +61,7 @@ class website_event(website_event): for registration in registrations: ticket = request.registry['event.event.ticket'].browse(cr, SUPERUSER_ID, int(registration['ticket_id']), context=context) cart_values = order.with_context(event_ticket_id=ticket.id)._cart_update(product_id=ticket.product_id.id, add_qty=1, registration_data=[registration]) - attendee_ids &= set(cart_values.get('attendees', [])) + attendee_ids |= set(cart_values.get('attendee_ids', [])) # free tickets -> order with amount = 0: auto-confirm, no checkout if not order.amount_total: diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index 3d269740e79f93a0c06ff02abebb2b8da7bbdbfa..be6ddd118400afcedd12c7dc9f90d0a4632a2574 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -12,6 +12,7 @@ from openerp.addons.web import http from openerp.addons.web.controllers.main import login_redirect from openerp.addons.web.http import request from openerp.addons.website.models.website import slug +from openerp.tools.translate import _ class WebsiteForum(http.Controller): @@ -317,6 +318,8 @@ class WebsiteForum(http.Controller): def post_create(self, forum, post_parent=None, post_type=None, **post): if not request.session.uid: return login_redirect() + if post_type == 'question' and not post.get('post_name', '').strip(): + return request.website.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Title should not be empty.')}) post_tag_ids = forum._tag_to_write_vals(post.get('post_tags', '')) new_question = request.env['forum.post'].create({ 'forum_id': forum.id, @@ -380,7 +383,9 @@ class WebsiteForum(http.Controller): @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/save', type='http', auth="user", methods=['POST'], website=True) def post_save(self, forum, post, **kwargs): - post_tags = forum._tag_to_write_vals(kwargs.get('post_tags', '')) + if 'post_name' in kwargs and not kwargs.get('post_name').strip(): + return request.website.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Title should not be empty.')}) + post_tags = forum._tag_to_write_vals(kwargs.get('post_tag', '')) vals = { 'tag_ids': post_tags, 'name': kwargs.get('post_name'), @@ -428,7 +433,7 @@ class WebsiteForum(http.Controller): def users(self, forum, page=1, **searches): User = request.env['res.users'] step = 30 - tag_count = User.search_count([('karma', '>', 1), ('website_published', '=', True)]) + tag_count = User.sudo().search_count([('karma', '>', 1), ('website_published', '=', True)]) pager = request.website.pager(url="/forum/%s/users" % slug(forum), total=tag_count, page=page, step=step, scope=30) user_obj = User.sudo().search([('karma', '>', 1), ('website_published', '=', True)], limit=step, offset=pager['offset'], order='karma DESC') # put the users in block of 3 to display them as a table diff --git a/addons/website_forum/static/src/css/website_forum.css b/addons/website_forum/static/src/css/website_forum.css index 395167cf6157e13dad072912afa2878e52162491..2e1a4ebaa38d6b95191b319ad6c77cc345fa88e9 100644 --- a/addons/website_forum/static/src/css/website_forum.css +++ b/addons/website_forum/static/src/css/website_forum.css @@ -1,113 +1,145 @@ .vote { min-width: 38px; - margin-right: 12px; } - .vote .vote_count { - font-size: 160%; - font-weight: bold; - line-height: 12px; } - .vote .vote_up, .vote .vote_down { - font-size: 130%; } + margin-right: 12px; +} +.vote .vote_count { + font-size: 160%; + font-weight: bold; + line-height: 12px; +} +.vote .vote_up, .vote .vote_down { + font-size: 130%; +} .author-box { min-width: 200px; padding: 4px; - background-color: whitesmoke; } + background-color: #f5f5f5; +} .question-block { - margin-left: 50px; } + margin-left: 50px; +} + +.question { + clear: left; +} .question .question-name { - font-size: 150%; } + font-size: 150%; +} .question .badge { - background-color: #cccccc; - margin-left: 4px; } + background-color: #ccc; + margin-left: 4px; +} .question .badge-active { - background-color: #428bca; } + background-color: #428bca; +} .question img { max-width: 600px; - height: auto !important; } + height: auto !important; +} .forum_answer img { max-width: 600px; - height: auto !important; } + height: auto !important; +} img.img-avatar { max-height: 40px; - margin-right: 10px; } + margin-right: 10px; +} .oe_grey { - background-color: #eeeeee; } + background-color: #eeeeee; +} .badge-gold { - color: #ffcc00; } + color: #ffcc00; +} .badge-silver { - color: #cccccc; } + color: #cccccc; +} .badge-bronze { - color: #eea91e; } + color: #eea91e; +} .oe_answer_true { - color: #3c763d; } + color: #3c763d; +} .forum_answer .oe_answer_true:hover { color: #4D964E; } .oe_answer_false { - color: #bbbbbb; } + color: #bbbbbb; +} .oe_answer_false:hover { color: #9E9C9C; } .favourite_question { - color: #bbbbbb !important; } + color: #bbbbbb !important; +} .forum_favourite_question { - color: #ffcc00 !important; } + color: #ffcc00 !important; +} a.no-decoration { cursor: pointer; - text-decoration: none !important; } + text-decoration: none !important; +} .faq-question:hover { text-decoration: none !important; - color: #428bca; } + color: #428bca; +} .oe_comment_grey { - background-color: whitesmoke; - padding: 4px; } + background-color: #f5f5f5; + padding: 4px; +} .close.comment_delete { - font-size: 16px; } + font-size: 16px; +} .country_flag { max-height: 16px; display: inline-block; - margin-left: 2px; } + margin-left: 2px; +} .tag_text .text-core .text-wrap textarea, .tag_text .text-core .text-wrap input, .tag_text .text-core .text-wrap .text-dropdown, .tag_text .text-core .text-wrap .text-prompt { - font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important; } + font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} .tag_text .text-core .text-wrap .text-tags .text-tag .text-button { font: 1.2em "Helvetica Neue", Helvetica, Arial, sans-serif !important; - height: 1.2em !important; } + height: 1.2em !important; +} .oe_forum_alert { position: absolute; margin-top: -30px; margin-left: 90px; width: 300px; - z-index: 9999; } + z-index: 9999; +} .oe_forum_email_required { position: absolute; margin-top: 155px; margin-left: 500px; margin-right: 100px; - z-index: 5; } + z-index: 5; +} button.btn-link.text-muted { - color: #999999; + color: #999; } diff --git a/addons/website_forum/static/src/css/website_forum.sass b/addons/website_forum/static/src/css/website_forum.sass index 1313516a08410fcaf6e381342005515e8286a8ee..4cfa6e97c74a5e518aca404052b0f0924868680d 100644 --- a/addons/website_forum/static/src/css/website_forum.sass +++ b/addons/website_forum/static/src/css/website_forum.sass @@ -16,6 +16,9 @@ .question-block margin-left: 50px +.question + clear: left + .question .question-name font-size: 150% diff --git a/addons/website_forum/static/src/js/website_forum.share.js b/addons/website_forum/static/src/js/website_forum.share.js index 3ad1f3d0e7db8ef71f38ddf1a341d2fd4e233812..766008b297ff5527665779cb3ee2587778a8bafe 100644 --- a/addons/website_forum/static/src/js/website_forum.share.js +++ b/addons/website_forum/static/src/js/website_forum.share.js @@ -5,6 +5,7 @@ var ajax = require('web.ajax'); var core = require('web.core'); var SocialShare = require('website.share'); var website = require('website.website'); +website.if_dom_contains('.website_forum', function () { var qweb = core.qweb; website.add_template_file('/website_forum/static/src/xml/website_forum_share_templates.xml'); @@ -61,6 +62,7 @@ website.ready().done(function() { } }); -return ForumShare; +}); +return {}; }); diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index f1d570d517d5be24b584e79156418ce357189dc8..3421be65390ccbbb8f67573507283fd977fd13ab 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -509,8 +509,8 @@ <li>Provide enough details and, if possible, give an example.</li> </ul> <form t-attf-action="/forum/#{slug(forum)}/new?post_type=question" method="post" role="form" class="tag_text"> - <input type="text" name="post_name" required="True" t-attf-value="#{post_name}" - class="form-control mb16" placeholder="Your Question Title..."/> + <input type="text" name="post_name" required="True" pattern=".*\S.*" t-attf-value="#{post_name}" + class="form-control mb16" placeholder="Your Question Title..." title="Title must not be empty"/> <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/> <textarea name="content" required="True" id="content" class="form-control load_editor" @@ -535,8 +535,8 @@ <h3 t-if="is_answer">Edit reply</h3> <form t-attf-action="/forum/#{slug(forum)}/post/#{slug(post)}/save" method="post" role="form" class="tag_text"> <div t-if="not is_answer"> - <input type="text" name="post_name" required="True" - t-attf-value="#{post.name}" class="form-control mb8" placeholder="Edit your Post"/> + <input type="text" name="post_name" required="True" pattern=".*\S.*" t-attf-value="#{post.name}" + class="form-control mb8" placeholder="Edit your Post" title="Title must not be empty"/> <h5 t-if="post.post_type == 'question'" class="mt20">Please enter a descriptive question (should finish by a '?')</h5> </div> <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/> diff --git a/addons/website_hr_recruitment/controllers/main.py b/addons/website_hr_recruitment/controllers/main.py index 1db4eb2f02fde52d05eba4b3eed473cc066b6431..4bef1dfe0d87cb08ce537157cbf24c451387bc18 100644 --- a/addons/website_hr_recruitment/controllers/main.py +++ b/addons/website_hr_recruitment/controllers/main.py @@ -116,22 +116,6 @@ class website_hr_recruitment(http.Controller): # Retro-compatibility for saas-3. "phone" field should be replace by "partner_phone" in the template in trunk. value['partner_phone'] = post.pop('phone', False) - # If the email is already known in our database, match it to the existing partner, else create a new one - Partner = env['res.partner'] - existing_partner = Partner.name_search(name=post.get('email_from').strip(), args=[('is_company', '=', False)], limit=1) - - if not existing_partner: - value_partner = { - 'name': post['partner_name'], - 'email': post['email_from'], - 'phone': value['partner_phone'], - } - partner = Partner.create(value_partner) - value['partner_id'] = partner.id - else: - value['partner_id'] = existing_partner[0][0] - - # Create applicant applicant = env['hr.applicant'].create(value) if post['ufile']: name = applicant.partner_name if applicant.partner_name else applicant.name diff --git a/addons/website_links/models/website_links.py b/addons/website_links/models/website_links.py index ac36ff2af8cb7ef96a8d2bb1ff6845384be36599..99148d1ae71b2268f68d13f0481ed7e4cc8d0f71 100644 --- a/addons/website_links/models/website_links.py +++ b/addons/website_links/models/website_links.py @@ -100,7 +100,7 @@ class website_links(models.Model): if attr: utms[key] = attr - self.redirected_url = '%s://%s%s?%s%s#%s' % (parsed.scheme, parsed.netloc, parsed.path, urlencode(utms), parsed.query, parsed.fragment) + self.redirected_url = '%s://%s%s?%s&%s#%s' % (parsed.scheme, parsed.netloc, parsed.path, urlencode(utms), parsed.query, parsed.fragment) @api.model @api.depends('url') diff --git a/addons/website_membership/__init__.py b/addons/website_membership/__init__.py index ee5959455ad41d51fbaff9d2ddcb980a25b622f2..9f86759e32be55377b85238f6ad6f71794bd4992 100644 --- a/addons/website_membership/__init__.py +++ b/addons/website_membership/__init__.py @@ -1 +1,2 @@ import controllers +import models diff --git a/addons/website_membership/controllers/main.py b/addons/website_membership/controllers/main.py index beedd50a04f17d83219460ae114d3691a7d0ac02..1df80c36c712a8aef6c92a57c4c3e05de3213cae 100644 --- a/addons/website_membership/controllers/main.py +++ b/addons/website_membership/controllers/main.py @@ -27,7 +27,7 @@ class WebsiteMembership(http.Controller): '/members/association/<membership_id>/country/<country_name>-<int:country_id>/page/<int:page>', '/members/association/<membership_id>/country/<int:country_id>/page/<int:page>', ], type='http', auth="public", website=True) - def members(self, membership_id=None, country_name=None, country_id=0, page=0, **post): + def members(self, membership_id=None, country_name=None, country_id=0, page=1, **post): cr, uid, context = request.cr, request.uid, request.context product_obj = request.registry['product.product'] country_obj = request.registry['res.country'] @@ -56,7 +56,7 @@ class WebsiteMembership(http.Controller): membership_line_ids = [] country_domain = ('free_member', '=', True) countries = partner_obj.read_group( - cr, uid, [country_domain, ("website_published", "=", True)], ["id", "country_id"], + cr, SUPERUSER_ID, [country_domain, ("website_published", "=", True)], ["id", "country_id"], groupby="country_id", orderby="country_id", context=request.context) countries_total = sum(country_dict['country_id_count'] for country_dict in countries) @@ -64,11 +64,12 @@ class WebsiteMembership(http.Controller): if country_id: line_domain.append(('partner.country_id', '=', country_id)) current_country = country_obj.read(cr, uid, country_id, ['id', 'name'], context) - if not any(x['country_id'][0] == country_id for x in countries): + if not any(x['country_id'][0] == country_id for x in countries if x['country_id']): countries.append({ 'country_id_count': 0, 'country_id': (country_id, current_country["name"]) }) + countries = filter(lambda d:d['country_id'], countries) countries.sort(key=lambda d: d['country_id'][1]) countries.insert(0, { @@ -82,15 +83,32 @@ class WebsiteMembership(http.Controller): # make sure we don't access to lines with unpublished membershipts line_domain.append(('membership_id', 'in', membership_ids)) - # displayed membership lines + limit = self._references_per_page + offset = limit * (page - 1) + + count_members = 0 + membership_line_ids = [] + # displayed non-free membership lines if membership_id != 'free': - membership_line_ids = membership_line_obj.search(cr, SUPERUSER_ID, line_domain, context=context) + count_members = membership_line_obj.search_count(cr, SUPERUSER_ID, line_domain, context=context) + if offset <= count_members: + membership_line_ids = tuple(membership_line_obj.search(cr, SUPERUSER_ID, line_domain, offset, limit, context=context)) membership_lines = membership_line_obj.browse(cr, uid, membership_line_ids, context=context) # TODO: Following line can be deleted in master. Kept for retrocompatibility. membership_lines = sorted(membership_lines, key=lambda x: x.membership_id.website_sequence) - partner_ids = [m.partner.id for m in membership_lines] - free_partner_ids = partner_obj.search(cr, SUPERUSER_ID, [('free_member', '=', True), ('website_published', '=', True)], context=context) - google_map_partner_ids = ",".join(map(str, partner_ids)) + page_partner_ids = set(m.partner.id for m in membership_lines) + + google_map_partner_ids = [] + if request.env.ref('website_membership.opt_index_google_map').customize_show: + membership_lines_ids = membership_line_obj.search(cr, uid, line_domain, context=context) + google_map_partner_ids = membership_line_obj.get_published_companies(cr, uid, membership_line_ids, limit=2000, context=context) + + search_domain = [('free_member', '=', True), ('website_published', '=', True)] + if post_name: + search_domain += ['|', ('name', 'ilike', post_name), ('website_description', 'ilike', post_name)] + if country_id: + search_domain += [('country_id', '=', country_id)] + free_partner_ids = partner_obj.search(cr, SUPERUSER_ID, search_domain, context=context) memberships_data = [] for membership_record in memberships: memberships_data.append({'id': membership_record.id, 'name': membership_record.name}) @@ -100,12 +118,23 @@ class WebsiteMembership(http.Controller): if free_partner_ids: memberships_data.append({'id': 'free', 'name': _('Free Members')}) if not membership_id or membership_id == 'free': - memberships_partner_ids['free'] = free_partner_ids + if count_members < offset + limit: + free_start = max(offset - count_members, 0) + free_end = max(offset + limit - count_members, 0) + memberships_partner_ids['free'] = free_partner_ids[free_start:free_end] + page_partner_ids |= set(memberships_partner_ids['free']) + google_map_partner_ids += free_partner_ids[:2000-len(google_map_partner_ids)] + count_members += len(free_partner_ids) + + google_map_partner_ids = ",".join(map(str, google_map_partner_ids)) + + partners = { p.id: p for p in partner_obj.browse(request.cr, SUPERUSER_ID, list(page_partner_ids), request.context)} - partners = dict((p.id, p) for p in partner_obj.browse(request.cr, SUPERUSER_ID, list(set(partner_ids + free_partner_ids)), request.context)) + base_url = '/members%s%s' % ('/association/%s' % membership_id if membership_id else '', + '/country/%s' % country_id if country_id else '') # request pager for lines - pager = request.website.pager(url="/members", total=len(membership_line_ids), page=page, step=self._references_per_page, scope=7, url_args=post) + pager = request.website.pager(url=base_url, total=count_members, page=page, step=limit, scope=7, url_args=post) values = { 'partners': partners, diff --git a/addons/website_membership/models/__init__.py b/addons/website_membership/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..58702d876e47839655f5f73493bfd4de629fcee4 --- /dev/null +++ b/addons/website_membership/models/__init__.py @@ -0,0 +1 @@ +import membership diff --git a/addons/website_membership/models/membership.py b/addons/website_membership/models/membership.py new file mode 100644 index 0000000000000000000000000000000000000000..feba5ce6ea623e76e29882e353e1f49a1dde88fb --- /dev/null +++ b/addons/website_membership/models/membership.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>). +# +# 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 <http://www.gnu.org/licenses/>. +# +############################################################################## + +from openerp.osv import osv, fields + +class membership_membership_line(osv.Model): + _inherit = 'membership.membership_line' + + def get_published_companies(self, cr, uid, ids, limit=None, context=None): + if not ids: + return [] + limit_clause = '' if limit is None else ' LIMIT %d' % limit + cr.execute('SELECT DISTINCT p.id \ + FROM res_partner p INNER JOIN membership_membership_line m \ + ON p.id = m.partner \ + WHERE website_published AND is_company AND m.id IN %s ' + limit_clause, (tuple(ids),)) + return [partner_id[0] for partner_id in cr.fetchall()] diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index 045116e3acc5fd68cac0b5f7b8ace07bbee79dbc..f5fc8d467416df3026f640c1d4119b606cca55a0 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -60,7 +60,7 @@ class sale_quote(http.Controller): 'message': message and int(message) or False, 'option': bool(filter(lambda x: not x.line_id, order.options)), 'order_valid': (not order.validity_date) or (now <= order.validity_date), - 'days_valid': max(days, 0), + 'days_valid': days, 'action': action } return request.website.render('website_quote.so_quotation', values) diff --git a/addons/website_quote/models/order.py b/addons/website_quote/models/order.py index bb549e8fbeb441f0c36211c2fe3abe6934bd0bfc..7a6e32db1b50bcd77703a63dbba4dbc238c28940 100644 --- a/addons/website_quote/models/order.py +++ b/addons/website_quote/models/order.py @@ -278,6 +278,8 @@ class sale_quote_option(osv.osv): 'name': product_obj.name, 'uom_id': product_obj.product_tmpl_id.uom_id.id, }) + if product_obj.description_sale: + vals['name'] += '\n'+product_obj.description_sale return {'value': vals} class sale_order_option(osv.osv): @@ -310,6 +312,8 @@ class sale_order_option(osv.osv): 'name': product_obj.name, 'uom_id': product_obj.product_tmpl_id.uom_id.id, }) + if product_obj.description_sale: + vals['name'] += '\n'+product_obj.description_sale return {'value': vals} class product_template(osv.Model): diff --git a/addons/website_quote/static/src/css/website_quotation.css b/addons/website_quote/static/src/css/website_quotation.css index c4bbd4d9c64b18e164e0ae0ebc1bf55cdbd47586..defa37feef5758f52131f1588acd76ed7881fe28 100644 --- a/addons/website_quote/static/src/css/website_quotation.css +++ b/addons/website_quote/static/src/css/website_quotation.css @@ -42,6 +42,11 @@ padding-left: 30px; font-size: 90%; } +@media (max-width: 991px) { + .o_website_quote .bs-sidebar { + position: static; + } +} @media (min-width: 992px) { .o_website_quote .bs-sidebar .nav > .active > ul { display: block; diff --git a/addons/website_quote/views/website_quotation.xml b/addons/website_quote/views/website_quotation.xml index ab00e2800d810ca1364d5911b129f0c3653b53cc..8d663dcf77576e2116bb7cea73e41e80cb0e3067 100644 --- a/addons/website_quote/views/website_quotation.xml +++ b/addons/website_quote/views/website_quotation.xml @@ -333,10 +333,12 @@ <t t-if="quotation.state not in ('draft','sent','cancel')">Your Order</t> <em t-esc="quotation.name"/> <small t-field="quotation.state"/> - <div groups="base.group_website_publisher" t-ignore="true" class="pull-right css_editable_mode_hidden hidden-print"> - <a class="btn btn-info" target="_blank" t-att-href="'/quote/%s/%s' % (quotation.id,quotation.access_token)+'?pdf=True'">Print</a> - <a t-if="quotation.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Update Quote</a> - <a t-if="quotation.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Back to Sale Order</a> + <div t-ignore="true" class="pull-right css_editable_mode_hidden hidden-print"> + <div groups="base.group_website_publisher"> + <a t-if="quotation.state not in ('manual')" class="btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Update Quote</a> + <a t-if="quotation.state in ('manual')" class=" btn btn-info" t-att-href="'/web#return_label=Website&model=%s&id=%s&action=%s&view_type=form' % (quotation._name, quotation.id, action)">Back to Sale Order</a> + </div> + <a class="btn btn-info" target="_blank" t-att-href="'/quote/%s/%s' % (quotation.id,quotation.access_token)+'?pdf=True'">Print</a> </div> </h1> </div> diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index fda3c1d2228d7956e1c0092e9d2495be164b1f7d..3d887272144e9a54b528c7a841b1faeb7d4a5d62 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -360,6 +360,10 @@ class website_sale(http.Controller): @http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True) def cart_update_json(self, product_id, line_id, add_qty=None, set_qty=None, display=True): order = request.website.sale_get_order(force_create=1) + if order.state != 'draft': + request.website.sale_reset() + return {} + value = order._cart_update(product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty) if not display: return None @@ -1006,10 +1010,13 @@ class website_sale(http.Controller): return ret @http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True) - def get_unit_price(self, product_ids, add_qty, **kw): + def get_unit_price(self, product_ids, add_qty, use_order_pricelist=False, **kw): cr, uid, context, pool = request.cr, request.uid, request.context, request.registry products = pool['product.product'].browse(cr, uid, product_ids, context=context) partner = pool['res.users'].browse(cr, uid, uid, context=context).partner_id - pricelist_id = request.website.get_current_pricelist(context=context).id + if use_order_pricelist: + pricelist_id = request.website.get_current_pricelist(context=context).id + else: + pricelist_id = partner.property_product_pricelist.id prices = pool['product.pricelist'].price_rule_get_multi(cr, uid, [], [(product, add_qty, partner) for product in products], context=context) return {product_id: prices[product_id][pricelist_id][0] for product_id in product_ids} diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 0214e9246e4a65bd19df2255f4ec3b6ab0e09471..3f35d27eab8f9041f350c299f92feaad3a81f194 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -6,6 +6,7 @@ from openerp import SUPERUSER_ID, tools from openerp.osv import osv, orm, fields from openerp.addons.web.http import request from openerp.tools.translate import _ +from openerp.exceptions import UserError class sale_order(osv.Model): @@ -82,6 +83,8 @@ class sale_order(osv.Model): quantity = 0 for so in self.browse(cr, uid, ids, context=context): + if so.state != 'draft': + raise UserError(_('It is forbidden to modify a sale order which is not in draft status')) if line_id is not False: line_ids = so._cart_find_product_line(product_id, line_id, context=context, **kwargs) if line_ids: @@ -292,7 +295,7 @@ class website(orm.Model): values['partner_id'] = partner.id sale_order_obj.write(cr, SUPERUSER_ID, [sale_order_id], values, context=context) - if flag_pricelist or values.get('fiscal_position_id') != fiscal_position: + if flag_pricelist or values.get('fiscal_position_id', False) != fiscal_position: update_pricelist = True if (code and code != sale_order.pricelist_id.code) or \ diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv index 110de4a75b0b443cb09af7fda1ac82aa3af822fb..47aca893c433256678f66dcd66e13fdbb7e957dc 100644 --- a/addons/website_sale/security/ir.model.access.csv +++ b/addons/website_sale/security/ir.model.access.csv @@ -15,4 +15,6 @@ access_product_attribute_value_public,product.attribute value public,product.mod access_product_attribute_price_public,product.attribute price public,product.model_product_attribute_price,,1,0,0,0 access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0 access_website_pricelist,website_pricelist,model_website_pricelist,,1,0,0,0 -access_website_pricelist_sale_manager,website_pricelist,model_website_pricelist,,base.group_sale_manager,1,1,1,1 \ No newline at end of file +access_website_pricelist_sale_manager,website_pricelist,model_website_pricelist,,base.group_sale_manager,1,1,1,1 +access_fiscal_position_public,fiscal position public,account.model_account_fiscal_position,base.group_portal,1,0,0,0 +access_payment_term,payment term public,account.model_account_payment_term,base.group_portal,1,0,0,0 \ No newline at end of file diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 70ae24f0a59552d998504b263d6cba0a34252709..5ac79ef1c5356ba28d053f2e45abaa6a5bc1c66d 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -69,7 +69,7 @@ $('.oe_website_sale').each(function () { var product_id = parseInt($input.data('product-id'),10); var product_ids = [product_id]; $dom_optional.each(function(){ - product_ids.push($(this).find('span[data-oe-model="product.product"]').data('oe-id')); + product_ids.push($(this).find('span[data-product-id]').data('product-id')); }); if (isNaN(value)) value = 0; @@ -79,14 +79,15 @@ $('.oe_website_sale').each(function () { else { var gup = ajax.jsonRpc("/shop/get_unit_price", 'call', { 'product_ids': product_ids, - 'add_qty': value}) + 'add_qty': value, + 'use_order_pricelist': true}) .then(function (res) { //basic case $dom.find('span.oe_currency_value').last().text(res[product_id].toFixed(2)); $dom.find('.text-danger').toggle(res[product_id]<default_price && (default_price-res[product_id] > default_price/100)); //optional case $dom_optional.each(function(){ - var id = $(this).find('span[data-oe-model="product.product"]').data('oe-id'); + var id = $(this).find('span[data-product-id]').data('product-id'); var price = parseFloat($(this).find(".text-danger > span.oe_currency_value").text()); $(this).find("span.oe_currency_value").last().text(res[id].toFixed(2)); $(this).find('.text-danger').toggle(res[id]<price && (price-res[id]>price/100)); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 919d29d5cce882e00ac8044f030ce1d937c93440..b5be678815b3fcae3cda7f2e97a7c22f53fad76c 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -307,8 +307,7 @@ <t t-call="website.layout"> <t t-set="additional_title" t-value="product.name"/> <div itemscope="itemscope" itemtype="http://schema.org/Product" id="wrap" class="js_sale"> - - <section class="container mt8"> + <section t-attf-class="container mt8 oe_website_sale #{(compute_currency(product.lst_price) - product.price) > 0.1 and 'discount'}" id="product_detail"> <div class="row"> <div class="col-sm-4"> <ol class="breadcrumb"> @@ -328,9 +327,6 @@ </t> </div> </div> - </section> - - <section t-attf-class="container oe_website_sale #{(compute_currency(product.lst_price) - product.price) > 0.1 and 'discount'}" id="product_detail"> <div class="row"> <div class="col-sm-7 col-md-7 col-lg-7"> <span itemprop="image" t-field="product.image" t-field-options='{"widget": "image", "class": "product_detail_img"}'/> @@ -729,7 +725,7 @@ <td> <div> <a t-attf-href="/shop/product/#{ slug(product.product_tmpl_id) }"> - <strong t-esc="product.name_get()[0][1]"/> + <strong t-field="product.display_name"/> </a> </div> <div class="text-muted" t-field="product.description_sale"/> diff --git a/addons/website_sale_delivery/views/website_sale_delivery.xml b/addons/website_sale_delivery/views/website_sale_delivery.xml index 4cd2e13792b6ca89beb9ee3b09a0949f63511857..d32ef491c0fa2f88aedbd365649edbd61070449f 100644 --- a/addons/website_sale_delivery/views/website_sale_delivery.xml +++ b/addons/website_sale_delivery/views/website_sale_delivery.xml @@ -40,6 +40,7 @@ "widget": "monetary", "display_currency": "website.pricelist_id.currency_id" }'/> + <div t-field="delivery.website_description" class="text-muted"/> </label> </t> </li> diff --git a/addons/website_sale_options/static/src/js/website_sale.js b/addons/website_sale_options/static/src/js/website_sale.js index ed979095b6b80d722b37a1dea0f1261d0a1c9005..c8a2175f384c64eadfab37bd7a02dac05ff4524f 100644 --- a/addons/website_sale_options/static/src/js/website_sale.js +++ b/addons/website_sale_options/static/src/js/website_sale.js @@ -71,7 +71,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit') }); $modal.on("change", 'input[name="add_qty"]', function (event) { - var product_id = $($modal.find('span.oe_price[data-oe-model="product.product"]').first()).data('oe-id'); + var product_id = $($modal.find('span.oe_price[data-product-id]')).first().data('product-id'); var default_price = parseInt($('.text-danger.oe_default_price > span.oe_currency_value').text()); var $dom = $(event.target).closest('tr'); var qty = $dom.find('input[name="add_qty"]').val(); diff --git a/addons/website_sale_options/views/templates.xml b/addons/website_sale_options/views/templates.xml index 5af23c8841d632e8246d633f06283adb22365290..1ce5886466da337182d1a7a044d7963938e2dcb3 100644 --- a/addons/website_sale_options/views/templates.xml +++ b/addons/website_sale_options/views/templates.xml @@ -19,7 +19,7 @@ </xpath> <xpath expr="//table[@id='cart_products']/tbody//td[last()]" position="inside"> <t t-if="line.linked_line_id"> - <span class="js_quantity text-muted" t-att-data-line-id="line.id" t-esc="int(line.product_uom_qty)"/> + <span class="js_quantity text-muted" t-att-data-line-id="line.id" t-att-data-product-id="line.product_id.id" t-esc="int(line.product_uom_qty)"/> </t> </xpath> </template> @@ -48,7 +48,7 @@ <span t-field="product.image_medium" t-field-options='{"widget": "image" }'/> </td> <td colspan="2"> - <strong t-field="product.name"/> + <strong t-field="product.display_name"/> <div class="text-muted"> <div t-field="product.description_sale"/> <div class="js_attributes"/> @@ -63,6 +63,7 @@ "display_currency": "user_id.partner_id.property_product_pricelist.currency_id" }'/> <span class="oe_price" style="white-space: nowrap;" + t-att-data-product-id="product.id" t-field="product.price" t-field-options='{ "widget": "monetary", diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 2cece16ca915d4904412338830beaf84962c0820..c1d0c68d02883df3c72e99d757c3f2cf606c07d6 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -10,7 +10,7 @@ var slides = require('website_slides.slides'); var _t = core._t; -$(document).ready(function () { +website.if_dom_contains('.oe_slide_js_upload', function () { website.add_template_file('/website_slides/static/src/xml/website_slides.xml'); diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css index 079d7a3b4b1115165d2fd29b4921f22992e225da..36b50ffa2d5c1c7dc5b58deba58e41d9081d4d54 100644 --- a/doc/_themes/odoodoc/static/style.css +++ b/doc/_themes/odoodoc/static/style.css @@ -7226,9 +7226,12 @@ td.field-body > ul { .stripe .section:not(.force-right) > [class*=highlight-] .highlight .il { color: #2aa198; } - .stripe .body > .section > .section { + .stripe .body > .section .section { border-top: 1px solid #eeeeee; } + .stripe .body > .section h1 + .section { + border-top: none; + } .stripe .section:not(.force-right) > h1, .stripe .section:not(.force-right) > h2, .stripe .section:not(.force-right) > h3, diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less index 376b422006a5765c80e977994fed384fee56b8e2..9d693d5624a229db3e0d1a7ecce30294495245f0 100644 --- a/doc/_themes/odoodoc/static/style.less +++ b/doc/_themes/odoodoc/static/style.less @@ -738,8 +738,15 @@ td.field-body { } } // separator above H2 - .body > .section > .section { - border-top: 1px solid @color-right; + .body > .section > { + .section { + border-top: 1px solid @color-right; + } + // but not if subsection directly follows an h1 (no introductory + // document paragraph) + h1 + .section { + border-top: none; + } } .section:not(.force-right) > h1, .section:not(.force-right) > h2, .section:not(.force-right) > h3, .section:not(.force-right) > h4, diff --git a/doc/business.rst b/doc/business.rst index def762578f69b0490ad58fc837edfd3ee9ebfc14..02a955c2efe4f6597293c33854da4e7f5a60954d 100644 --- a/doc/business.rst +++ b/doc/business.rst @@ -5,3 +5,5 @@ Business Mementoes .. toctree:: Accounting Memento (US GAAP) <https://odoo.com/documentation/functional/accounting.html> + Double-Entry Inventory <https://www.odoo.com/documentation/functional/double-entry.html> + Inventory Valuations <https://www.odoo.com/documentation/functional/valuation.html> diff --git a/doc/cla/corporate/avanzosc.md b/doc/cla/corporate/avanzosc.md new file mode 100644 index 0000000000000000000000000000000000000000..a2fc89b8851ed7759e45295037d5a050d280563c --- /dev/null +++ b/doc/cla/corporate/avanzosc.md @@ -0,0 +1,21 @@ +Spain, 2015-04-22 + +Avanzosc, S.L. agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Ana Juaristi anajuaristi@avanzosc.es https://github.com/anajuaristi + +List of contributors: + +Ana Juaristi anajuaristi@avanzosc.es https://github.com/anajuaristi +Ainara Galdona ainaragaldona@avanzosc.es https://github.com/agaldona +Alfredo de la Fuente alfredodelafuente@avanzosc.es https://github.com/alfredoavanzosc +Daniel Campos danielcampos@avanzosc.es https://github.com/Daniel-CA +Ibone Juaristi ibonejuaristi@avanzosc.es https://github.com/ibonejuaristi +Mikel Arregi mikelarregi@avanzosc.es https://github.com/mikelarre +Oihane Crucelaegui oihanecrucelaegi@avanzosc.es https://github.com/oihane diff --git a/doc/cla/corporate/iris-solutions.md b/doc/cla/corporate/iris-solutions.md new file mode 100644 index 0000000000000000000000000000000000000000..971712bf53e108edb6a16eabc701f77720259ad6 --- /dev/null +++ b/doc/cla/corporate/iris-solutions.md @@ -0,0 +1,13 @@ +France, 2015-04-18 + +Iris-solutions agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this declaration. + +Signed, + +Alexandre Laidin alexandre.laidin@iris-solutions.fr https://github.com/iris-solutions + +List of contributors: + +Alexandre Laidin alexandre.laidin@iris-solutions.fr https://github.com/alaidin diff --git a/doc/cla/corporate/loyal.md b/doc/cla/corporate/loyal.md new file mode 100644 index 0000000000000000000000000000000000000000..d3b4fcb5da890105a45e0cadfe58df1ce2f7423b --- /dev/null +++ b/doc/cla/corporate/loyal.md @@ -0,0 +1,15 @@ +China, 2015-03-13 + +Loyal agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Kevin Wang kevin_327@163.com https://github.com/kevin3274 + +List of contributors: + +Kevin Wang kevin@loyal-info.com https://github.com/kevin3274 diff --git a/doc/cla/corporate/makinacorpus.md b/doc/cla/corporate/makinacorpus.md new file mode 100644 index 0000000000000000000000000000000000000000..3f1eb843196344cb0d9003bf344ca327c91f6e6b --- /dev/null +++ b/doc/cla/corporate/makinacorpus.md @@ -0,0 +1,16 @@ +France, 2015-03-20 + +Makina Corpus agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Mathieu Le Marec - Pasquet, mpa@makina-corpus.com, https://github.com/kiorky + +List of contributors: + +Mathieu Le Marec - Pasquet, mpa@makina-corpus.com, https://github.com/kiorky +Makina Corpus, sysadmin@makina-corpus.com, http://www.makina-corpus.com diff --git a/doc/cla/corporate/numerigraphe.md b/doc/cla/corporate/numerigraphe.md new file mode 100644 index 0000000000000000000000000000000000000000..ecd5379fc870d442f8ada9f73c97453e8401ce06 --- /dev/null +++ b/doc/cla/corporate/numerigraphe.md @@ -0,0 +1,16 @@ +France, 2015-04-22 + +Numérigraphe agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Lionel Sausin ls@numerigraphe.com https://github.com/clonedagain + +List of contributors: + +Lionel Sausin ls@numerigraphe.com https://github.com/clonedagain +Loïc Bellier lb@numerigraphe.com https://github.com/lbellier diff --git a/doc/cla/corporate/roomsfor.md b/doc/cla/corporate/roomsfor.md new file mode 100644 index 0000000000000000000000000000000000000000..f2aa1d0c2cefee9a58bbaf03f9f36f2d03bff57c --- /dev/null +++ b/doc/cla/corporate/roomsfor.md @@ -0,0 +1,13 @@ +Hong Kong, 2015-04-27 + +Rooms For (Hong Kong) Limited agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this declaration. + +Signed, + +Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro + +List of contributors: + +Yoshi Tashiro tashiro@roomsfor.hk https://github.com/yostashiro diff --git a/doc/cla/corporate/syleam.md b/doc/cla/corporate/syleam.md new file mode 100644 index 0000000000000000000000000000000000000000..75ac0025efe7d8132e52bcb65551b539229fd890 --- /dev/null +++ b/doc/cla/corporate/syleam.md @@ -0,0 +1,17 @@ +France, 2015-04-08 + +Syleam agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Sylvain Garancher sylvain.garancher@syleam.fr https://github.com/sylvain-garancher + +List of contributors: + +Sébastien Lange sebastien.lange@syleam.fr https://github.com/alnslang +Sylvain Garancher sylvain.garancher@syleam.fr https://github.com/sylvain-garancher +Alexandre Moreau alexandre.moreau@syleam.fr https://github.com/a-moreau diff --git a/doc/cla/corporate/taktik.md b/doc/cla/corporate/taktik.md new file mode 100644 index 0000000000000000000000000000000000000000..bcf2e8796f9169e231a469542967d8422cbcb1fe --- /dev/null +++ b/doc/cla/corporate/taktik.md @@ -0,0 +1,20 @@ +Belgium, 2015-04-23 + +TAKTIK SA/NV agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Maxime Vanderhaeghe mv@taktik.be https://github.com/mv-taktik + +List of contributors: + +Adil Houmadi ah@taktik.be https://github.com/ah-taktik +David Lefever dl@taktik.be https://github.com/lefeverd +François Vanderborght fv@taktik.be https://github.com/fv-taktik +Kevin Goris kg@taktik.be https://github.com/kg-taktik +Matthias Vercruysse mve@taktik.be https://github.com/mve-taktik +Maxime Vanderhaeghe mv@taktik.be https://github.com/mv-taktik diff --git a/doc/cla/corporate/teclib.md b/doc/cla/corporate/teclib.md new file mode 100644 index 0000000000000000000000000000000000000000..a35c0a3277d1c7282e76b9ab0fbaef86b7aca616 --- /dev/null +++ b/doc/cla/corporate/teclib.md @@ -0,0 +1,13 @@ +France, 2015-04-14 + +TecLib agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this declaration. + +Signed, + +Laurent Destailleur ldestailleur@teclib.com https://github.com/eldy + +List of contributors: + +Laurent Destailleur ldestailleur@teclib.com https://github.com/eldy diff --git a/doc/cla/individual/PCatinean.md b/doc/cla/individual/PCatinean.md new file mode 100644 index 0000000000000000000000000000000000000000..4c846d97335d8cc0a1a5c67d3bb89ce0503a54e5 --- /dev/null +++ b/doc/cla/individual/PCatinean.md @@ -0,0 +1,11 @@ +Romania, 2015-04-01 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Paul Catinean paulcatinean@gmail.com https://github.com/PCatinean diff --git a/doc/cla/individual/alejandrosantana.md b/doc/cla/individual/alejandrosantana.md new file mode 100644 index 0000000000000000000000000000000000000000..aa3517ddb2b98fde84b80abf58dee7c87a6e862f --- /dev/null +++ b/doc/cla/individual/alejandrosantana.md @@ -0,0 +1,11 @@ +Spain, 2015-04-09 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Alejandro Santana tech@alejandrosantana.eu https://github.com/alejandrosantana diff --git a/doc/cla/individual/andreparames.md b/doc/cla/individual/andreparames.md new file mode 100644 index 0000000000000000000000000000000000000000..e51774f799bbdcf42f35b4af5b8fbfe15430b71f --- /dev/null +++ b/doc/cla/individual/andreparames.md @@ -0,0 +1,11 @@ +Portugal, 2015-04-23 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +André Pereira github@andreparames.com https://github.com/andreparames diff --git a/doc/cla/individual/belkacem77.md b/doc/cla/individual/belkacem77.md new file mode 100644 index 0000000000000000000000000000000000000000..aa293693913deeee82fb7f04fc0cf458c577fff2 --- /dev/null +++ b/doc/cla/individual/belkacem77.md @@ -0,0 +1,11 @@ +Algeria, 04 April 2015 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Belkacem Mohammed belkacem77@gmail.com https://github.com/belkacem77 diff --git a/doc/cla/individual/colinnewell.md b/doc/cla/individual/colinnewell.md new file mode 100644 index 0000000000000000000000000000000000000000..89a4ab891bbb5512cac044324cfaa1c3bd23d23e --- /dev/null +++ b/doc/cla/individual/colinnewell.md @@ -0,0 +1,12 @@ +United Kingdom, 2015-05-18 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Colin Newell colin.newell@gmail.com https://github.com/colinnewell + diff --git a/doc/cla/individual/cubells.md b/doc/cla/individual/cubells.md new file mode 100644 index 0000000000000000000000000000000000000000..b08faab0607c2caf695d40680ce96a57e9b774fc --- /dev/null +++ b/doc/cla/individual/cubells.md @@ -0,0 +1,11 @@ +Spain, 2015-04-20 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Vicent Cubells vicent@vcubells.net https://github.com/cubells diff --git a/doc/cla/individual/daniel-ca.md b/doc/cla/individual/daniel-ca.md new file mode 100644 index 0000000000000000000000000000000000000000..8a1c53c1102959bda9acb8ffe82ec122658c704f --- /dev/null +++ b/doc/cla/individual/daniel-ca.md @@ -0,0 +1,10 @@ +Spain, 2015-04-23 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, +Daniel Campos danielcampos@avanzosc.es https://github.com/Daniel-CA diff --git a/doc/cla/individual/jeffery.md b/doc/cla/individual/jeffery.md new file mode 100644 index 0000000000000000000000000000000000000000..5063b81c4df3f75ba641034b7111bc8cb7032243 --- /dev/null +++ b/doc/cla/individual/jeffery.md @@ -0,0 +1,11 @@ +China, 2015-04-25 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Jeffery Chen Fan jeffery9@gmail.com https://github.com/jeffery9 diff --git a/doc/cla/individual/kiorky.md b/doc/cla/individual/kiorky.md new file mode 100644 index 0000000000000000000000000000000000000000..6cbf661efcf0966d35f4acfa8c30c193c8fe595d --- /dev/null +++ b/doc/cla/individual/kiorky.md @@ -0,0 +1,11 @@ +France, 2015-03-20 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Mathieu Le Marec - Pasquet, kiorky@cryptelium.net, https://github.com/kiorky diff --git a/doc/cla/individual/mvaled.md b/doc/cla/individual/mvaled.md new file mode 100644 index 0000000000000000000000000000000000000000..e9dd0add9523c777276fff8332c2892e29065b3f --- /dev/null +++ b/doc/cla/individual/mvaled.md @@ -0,0 +1,11 @@ +La Habana, 2015-04-30 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Manuel Vázquez Acosta mva.led@gmail.com https://github.com/mvaled diff --git a/doc/cla/individual/nedaszilinskas.md b/doc/cla/individual/nedaszilinskas.md new file mode 100644 index 0000000000000000000000000000000000000000..3a980687556676b4d07c7125a7baf24f5d6e5300 --- /dev/null +++ b/doc/cla/individual/nedaszilinskas.md @@ -0,0 +1,11 @@ +Lithuania, 2015-04-18 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Nedas Žilinskas nedas.zilinskas@gmail.com https://github.com/nedaszilinskas \ No newline at end of file diff --git a/doc/cla/individual/samuellefever.md b/doc/cla/individual/samuellefever.md new file mode 100644 index 0000000000000000000000000000000000000000..83fb7bc6bd53a60386e35709e9f8a1311d7f1ca6 --- /dev/null +++ b/doc/cla/individual/samuellefever.md @@ -0,0 +1,11 @@ +Belgium, 2015-04-14 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Samuel Lefever sam@niboo.be https://github.com/samuellefever \ No newline at end of file diff --git a/doc/cla/individual/shingonoide.md b/doc/cla/individual/shingonoide.md new file mode 100644 index 0000000000000000000000000000000000000000..af4e340188d903acfc7fe898e37ab35f2eb5a3aa --- /dev/null +++ b/doc/cla/individual/shingonoide.md @@ -0,0 +1,11 @@ +Brazil, 2015-04-28 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Rui Andrada shingonoide@gmail.com https://github.com/shingonoide diff --git a/doc/cla/sign-cla.md b/doc/cla/sign-cla.md index 3eb4bd9a66abed8211cb5cc32f87ba07731f6fef..dab6b3061b919f8ea8e1288af02a79f47c26ebed 100644 --- a/doc/cla/sign-cla.md +++ b/doc/cla/sign-cla.md @@ -42,7 +42,7 @@ signature is merged. ## If you work for a company -1. Read the [Corporate Contributor License Agreement](icla-1.0.md) +1. Read the [Corporate Contributor License Agreement](ccla-1.0.md) 2. Modify your current pull request, or make a new pull request on [odoo/odoo](/odoo/odoo), adding a new file `<lowercase-company-name>.md` diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst index d7d6e4c02ea6f41ee27db78136f2780d9766a8b8..00c5b103462c0734a41c0eb3d70363d73a016238 100644 --- a/doc/howtos/backend.rst +++ b/doc/howtos/backend.rst @@ -764,6 +764,7 @@ method should simply set the value of the field to compute on every record in name = fields.Char(compute='_compute_name') + @api.multi def _compute_name(self): for record in self: record.name = str(random.randint(1, 1e6)) diff --git a/doc/howtos/backend/exercise-gantt b/doc/howtos/backend/exercise-gantt index dc4033e18de9a7e5827ba9f9cd071b7ec409df3f..273854b197d77d6cc528908f431c8e0eb5df88b3 100644 --- a/doc/howtos/backend/exercise-gantt +++ b/doc/howtos/backend/exercise-gantt @@ -35,7 +35,7 @@ Index: addons/openacademy/views/openacademy.xml =================================================================== --- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:21:46.543015785 +0200 +++ addons/openacademy/views/openacademy.xml 2014-08-28 14:21:46.539015785 +0200 -@@ -145,11 +145,24 @@ +@@ -145,11 +145,23 @@ </field> </record> @@ -44,10 +44,9 @@ Index: addons/openacademy/views/openacademy.xml + <field name="model">openacademy.session</field> + <field name="arch" type="xml"> + <gantt string="Session Gantt" color="course_id" -+ date_start="start_date" date_delay="hours"> -+ <level object="res.partner" link="instructor_id"> -+ <field name="name"/> -+ </level> ++ date_start="start_date" date_delay="hours" ++ default_group_by='instructor_id'> ++ <field name="name"/> + </gantt> + </field> + </record> diff --git a/doc/howtos/backend/exercise-state-workflow-actions b/doc/howtos/backend/exercise-state-workflow-actions index 826ec5ce870a58bda3897fa3ee627edc9b9b3746..fe9a0c8a7960062e705db7a83ed2cf1344e5a9d6 100644 --- a/doc/howtos/backend/exercise-state-workflow-actions +++ b/doc/howtos/backend/exercise-state-workflow-actions @@ -5,7 +5,7 @@ Index: addons/openacademy/views/session_workflow.xml =================================================================== --- addons.orig/openacademy/views/session_workflow.xml 2014-08-26 17:26:17.339783114 +0200 +++ addons/openacademy/views/session_workflow.xml 2014-08-26 17:26:17.331783114 +0200 -@@ -6,24 +6,53 @@ +@@ -6,24 +6,50 @@ <field name="on_create">True</field> </record> @@ -13,8 +13,7 @@ Index: addons/openacademy/views/session_workflow.xml + <field name="name">Set session to Draft</field> + <field name="model_id" ref="model_openacademy_session"/> + <field name="code"> -+recs = self.browse(cr, uid, context['active_ids'], context=context) -+recs.action_draft() ++model.search([('id', 'in', context['active_ids'])]).action_draft() + </field> + </record> <record model="workflow.activity" id="draft"> @@ -32,8 +31,7 @@ Index: addons/openacademy/views/session_workflow.xml + <field name="name">Set session to Confirmed</field> + <field name="model_id" ref="model_openacademy_session"/> + <field name="code"> -+recs = self.browse(cr, uid, context['active_ids'], context=context) -+recs.action_confirm() ++model.search([('id', 'in', context['active_ids'])]).action_confirm() + </field> </record> <record model="workflow.activity" id="confirmed"> @@ -50,8 +48,7 @@ Index: addons/openacademy/views/session_workflow.xml + <field name="name">Set session to Done</field> + <field name="model_id" ref="model_openacademy_session"/> + <field name="code"> -+recs = self.browse(cr, uid, context['active_ids'], context=context) -+recs.action_done() ++model.search([('id', 'in', context['active_ids'])]).action_done() + </field> </record> <record model="workflow.activity" id="done"> diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 1d27eb340b2b4bb615b7c38b7c70f24e65daa8c5..7f1df2233d5c162d91de06a0d71adcbeea2cd458 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -136,9 +136,10 @@ class ir_http(osv.AbstractModel): # This is done first as the attachment path may # not match any HTTP controller - attach = self._serve_attachment() - if attach: - return attach + if isinstance(exception, werkzeug.exceptions.HTTPException) and exception.code == 404: + attach = self._serve_attachment() + if attach: + return attach # Don't handle exception but use werkeug debugger if server in --dev mode if openerp.tools.config['dev_mode']: diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 4fbcb32b3e47e9e3f256ba07bdb6c3d03a0f3b17..229ec95aae294f8e026569b831abc69998166822 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -1046,7 +1046,7 @@ class ir_model_data(osv.osv): if action_id and res_id: model_obj.write(cr, uid, [res_id], values, context=context) - self.write(cr, uid, [action_id], { + self.write(cr, SUPERUSER_ID, [action_id], { 'date_update': time.strftime('%Y-%m-%d %H:%M:%S'), },context=context) elif res_id: @@ -1056,14 +1056,14 @@ class ir_model_data(osv.osv): for table in model_obj._inherits: inherit_id = model_obj.browse(cr, uid, res_id,context=context)[model_obj._inherits[table]] - self.create(cr, uid, { + self.create(cr, SUPERUSER_ID, { 'name': xml_id + '_' + table.replace('.', '_'), 'model': table, 'module': module, 'res_id': inherit_id.id, 'noupdate': noupdate, },context=context) - self.create(cr, uid, { + self.create(cr, SUPERUSER_ID, { 'name': xml_id, 'model': model, 'module':module, @@ -1078,14 +1078,14 @@ class ir_model_data(osv.osv): for table in model_obj._inherits: inherit_id = model_obj.browse(cr, uid, res_id,context=context)[model_obj._inherits[table]] - self.create(cr, uid, { + self.create(cr, SUPERUSER_ID, { 'name': xml_id + '_' + table.replace('.', '_'), 'model': table, 'module': module, 'res_id': inherit_id.id, 'noupdate': noupdate, },context=context) - self.create(cr, uid, { + self.create(cr, SUPERUSER_ID, { 'name': xml_id, 'model': model, 'module': module, diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index a40d6cc0a25182690e98e971eaed8efb1768d842..7fe0d8cc75d11be42b71c2ccd6f5e003c6f84001 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -876,9 +876,9 @@ class MonetaryConverter(osv.AbstractModel): pre = post = u'' if display_currency.position == 'before': - pre = u'{symbol} ' + pre = u'{symbol}\N{NO-BREAK SPACE}' else: - post = u' {symbol}' + post = u'\N{NO-BREAK SPACE}{symbol}' return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format( formatted_amount, @@ -1037,9 +1037,9 @@ class QwebWidgetMonetary(osv.AbstractModel): ) pre = post = u'' if display.position == 'before': - pre = u'{symbol} ' + pre = u'{symbol}\N{NO-BREAK SPACE}' else: - post = u' {symbol}' + post = u'\N{NO-BREAK SPACE}{symbol}' return u'{pre}{0}{post}'.format( formatted_amount, pre=pre, post=post diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index a9a2dcd073c4e7a9ba1d6c29eb17ec5b64c09062..e4db58481a641311a1f16fb137f0746562ecc998 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -295,8 +295,8 @@ class ir_sequence(models.Model): return False force_company = self.env.context.get('force_company') if not force_company: - force_company = self.env.user.company_id - preferred_sequences = [s for s in seq_ids if s.company_id and s.company_id == force_company] + force_company = self.env.user.company_id.id + preferred_sequences = [s for s in seq_ids if s.company_id and s.company_id.id == force_company] seq_id = preferred_sequences[0] if preferred_sequences else seq_ids[0] return seq_id._next() diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 2510ebac3dfc84ba816efe775c32d227e44546fc..77a718bab6cb1b99432323887c431e86917d716d 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -1140,15 +1140,15 @@ class view(osv.osv): if model_value._obj==node_obj: _Node_Field=model_key _Model_Field=model_value._fields_id - flag=False for node_key,node_value in _Node_Obj._columns.items(): if node_value._type=='one2many': if node_value._obj==conn_obj: - if src_node in _Arrow_Obj._columns and flag: + # _Source_Field = "Incoming Arrows" (connected via des_node) + if node_value._fields_id == des_node: _Source_Field=node_key - if des_node in _Arrow_Obj._columns and not flag: + # _Destination_Field = "Outgoing Arrows" (connected via src_node) + if node_value._fields_id == src_node: _Destination_Field=node_key - flag = True datas = _Model_Obj.read(cr, uid, id, [],context) for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]): diff --git a/openerp/addons/base/res/ir_property.py b/openerp/addons/base/res/ir_property.py index 116fc9b4876a84fd416c5f77e6e47ec2eb46d51b..f6597de537cd3492b2299bfe1e4c39da8d2ab96c 100644 --- a/openerp/addons/base/res/ir_property.py +++ b/openerp/addons/base/res/ir_property.py @@ -227,7 +227,7 @@ class ir_property(osv.osv): # retrieve the properties corresponding to the given record ids self._cr.execute("SELECT id FROM ir_model_fields WHERE name=%s AND model=%s", (name, model)) field_id = self._cr.fetchone()[0] - company_id = self.env['res.company']._company_default_get(model, field_id) + company_id = self.env.context.get('force_company') or self.env['res.company']._company_default_get(model, field_id) refs = {('%s,%s' % (model, id)): id for id in values} props = self.search([ ('fields_id', '=', field_id), @@ -260,12 +260,20 @@ class ir_property(osv.osv): @api.model def search_multi(self, name, model, operator, value): """ Return a domain for the records that match the given condition. """ + default_matches = False + include_zero = False + field = self.env[model]._fields[name] if field.type == 'many2one': comodel = field.comodel_name def makeref(value): return value and '%s,%s' % (comodel, value) - if operator in ('=', '!=', '<=', '<', '>', '>='): + if operator == "=": + value = makeref(value) + # if searching properties not set, search those not in those set + if value is False: + default_matches = True + elif operator in ('!=', '<=', '<', '>', '>='): value = makeref(value) elif operator in ('in', 'not in'): value = map(makeref, value) @@ -275,6 +283,27 @@ class ir_property(osv.osv): target_names = target.name_search(value, operator=operator, limit=None) target_ids = map(itemgetter(0), target_names) operator, value = 'in', map(makeref, target_ids) + elif field.type in ('integer', 'float'): + # No record is created in ir.property if the field's type is float or integer with a value + # equal to 0. Then to match with the records that are linked to a property field equal to 0, + # the negation of the operator must be taken to compute the goods and the domain returned + # to match the searched records is just the opposite. + if value == 0 and operator == '=': + operator = '!=' + include_zero = True + elif value <= 0 and operator == '>=': + operator = '<' + include_zero = True + elif value <= 0 and operator == '>': + operator = '<=' + include_zero = True + elif value >= 0 and operator == '<=': + operator = '>' + include_zero = True + elif value >= 0 and operator == '<': + operator = '>=' + include_zero = True + # retrieve the properties that match the condition domain = self._get_domain(name, model) @@ -284,7 +313,6 @@ class ir_property(osv.osv): # retrieve the records corresponding to the properties that match good_ids = [] - default_matches = False for prop in props: if prop.res_id: res_model, res_id = prop.res_id.split(',') @@ -292,7 +320,9 @@ class ir_property(osv.osv): else: default_matches = True - if default_matches: + if include_zero: + return [('id', 'not in', good_ids)] + elif default_matches: # exclude all records with a property that does not match all_ids = [] props = self.search(domain + [('res_id', '!=', False)]) diff --git a/openerp/addons/base/res/res.country.state.csv b/openerp/addons/base/res/res.country.state.csv index d6b902e493ba47c28b59812a0aca90388ab0dac9..1cb280a6121c7ae0dde83238570c8a59ddb2c42a 100644 --- a/openerp/addons/base/res/res.country.state.csv +++ b/openerp/addons/base/res/res.country.state.csv @@ -1,4 +1,12 @@ "id","country_id:id","name","code" +state_au_1,au,"Australian Capital Territory","ACT" +state_au_2,au,"New South Wales","NSW" +state_au_3,au,"Northern Territory","NT" +state_au_4,au,"Queensland","QLD" +state_au_5,au,"South Australia","SA" +state_au_6,au,"Tasmania","TAS" +state_au_7,au,"Victoria","VIC" +state_au_8,au,"Western Australia","WA" state_us_1,us,"Alabama","AL" state_us_2,us,"Alaska","AK" state_us_3,us,"Arizona","AZ" diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 7e4b651e7eb3fe43884eb969cfa2daf59aa5e91a..1f2e64d08e6236b9d7aae9e66d5995295cb4a9d7 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -280,7 +280,7 @@ class res_users(osv.osv): # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz'] # User can read a few of his own fields - SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update'] + SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update', 'action_id'] def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): def override_password(o): @@ -335,6 +335,8 @@ class res_users(osv.osv): # if partner is global we keep it that way if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: user.partner_id.write({'company_id': user.company_id.id}) + # clear default ir values when company changes + self.pool['ir.values'].get_defaults_dict.clear_cache(self.pool['ir.values']) # clear caches linked to the users self.pool['ir.model.access'].call_cache_clearing_methods(cr) clear = partial(self.pool['ir.rule'].clear_cache, cr) diff --git a/openerp/addons/test_converter/tests/test_html.py b/openerp/addons/test_converter/tests/test_html.py index aef7badfe3a50feee1b5f9063dc40b8464c1a183..2f50602c11566ebf3295a844e4c7117981d1127d 100644 --- a/openerp/addons/test_converter/tests/test_html.py +++ b/openerp/addons/test_converter/tests/test_html.py @@ -130,10 +130,10 @@ class TestCurrencyExport(TestExport): 'data-oe-field="value" data-oe-type="monetary" ' 'data-oe-expression="obj.value">' '<span class="oe_currency_value">0.12</span>' - ' {symbol}</span>'.format( + u'\N{NO-BREAK SPACE}{symbol}</span>'.format( obj=obj, symbol=currency.symbol.encode('utf-8') - ),) + ).encode('utf-8'),) def test_currency_pre(self): currency = self.create( @@ -147,12 +147,12 @@ class TestCurrencyExport(TestExport): '<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" ' 'data-oe-field="value" data-oe-type="monetary" ' 'data-oe-expression="obj.value">' - '{symbol} ' + u'{symbol}\N{NO-BREAK SPACE}' '<span class="oe_currency_value">0.12</span>' '</span>'.format( obj=obj, symbol=currency.symbol.encode('utf-8') - ),) + ).encode('utf-8'),) def test_currency_precision(self): """ Precision should be the currency's, not the float field's @@ -168,10 +168,10 @@ class TestCurrencyExport(TestExport): 'data-oe-field="value" data-oe-type="monetary" ' 'data-oe-expression="obj.value">' '<span class="oe_currency_value">0.12</span>' - ' {symbol}</span>'.format( + u'\N{NO-BREAK SPACE}{symbol}</span>'.format( obj=obj, symbol=currency.symbol.encode('utf-8') - ),) + ).encode('utf-8'),) class TestTextExport(TestBasicExport): def test_text(self): diff --git a/openerp/addons/test_inherits/tests/test_inherits.py b/openerp/addons/test_inherits/tests/test_inherits.py index 7ff5dee814b76a2bb0970c37dcc72839ac3ca3e5..d45882b5258cdf16a02585a116fc97d8b330ae3e 100644 --- a/openerp/addons/test_inherits/tests/test_inherits.py +++ b/openerp/addons/test_inherits/tests/test_inherits.py @@ -16,6 +16,11 @@ class test_inherits(common.TransactionCase): self.assertEqual(pallet.field_in_box, 'box') self.assertEqual(pallet.field_in_pallet, 'pallet') + def test_read_3_levels_inherits(self): + """ Check that we can read an inherited field on 3 levels """ + pallet = self.env.ref('test_inherits.pallet_a') + self.assertEqual(pallet.read(['name']), [{'id': pallet.id, 'name': 'Unit A'}]) + def test_write_3_levels_inherits(self): """ Check that we can create an inherits on 3 levels """ pallet = self.env.ref('test_inherits.pallet_a') diff --git a/openerp/api.py b/openerp/api.py index 1c1287653fa96b2745415f08a48c9a7c60caa317..28ace875a867b336fa3a4be7a68b08ae3951e695 100644 --- a/openerp/api.py +++ b/openerp/api.py @@ -68,7 +68,7 @@ from pprint import pformat from weakref import WeakSet from werkzeug.local import Local, release_local -from openerp.tools import frozendict +from openerp.tools import frozendict, classproperty _logger = logging.getLogger(__name__) @@ -674,6 +674,10 @@ class Environment(object): """ _local = Local() + @classproperty + def envs(cls): + return cls._local.environments + @classmethod @contextmanager def manage(cls): @@ -699,7 +703,7 @@ class Environment(object): args = (cr, uid, context) # if env already exists, return it - env, envs = None, cls._local.environments + env, envs = None, cls.envs for env in envs: if env.args == args: return env @@ -903,6 +907,13 @@ class Environment(object): finally: self.all.recompute = tmp + @property + def recompute_old(self): + return self.all.recompute_old + + def clear_recompute_old(self): + del self.all.recompute_old[:] + class Environments(object): """ A common object for all environments in a request. """ @@ -911,6 +922,7 @@ class Environments(object): self.todo = {} # recomputations {field: [records]} self.mode = False # flag for draft/onchange self.recompute = True + self.recompute_old = [] # list of old api compute fields to recompute def add(self, env): """ Add the environment `env`. """ diff --git a/openerp/cli/server.py b/openerp/cli/server.py index f4fa608c5eacab8f49f8211043b30e59793a1340..ca7403b2de843c7fe4d7a41a5e9e9cc50345dd4b 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -50,12 +50,11 @@ __version__ = openerp.release.version _logger = logging.getLogger('openerp') def check_root_user(): - """ Exit if the process's user is 'root' (on POSIX system).""" + """Warn if the process's user is 'root' (on POSIX system).""" if os.name == 'posix': import pwd - if pwd.getpwuid(os.getuid())[0] == 'root' : - sys.stderr.write("Running as user 'root' is a security risk, aborting.\n") - sys.exit(1) + if pwd.getpwuid(os.getuid())[0] == 'root': + sys.stderr.write("Running as user 'root' is a security risk.\n") def check_postgres_user(): """ Exit if the configured database user is 'postgres'. diff --git a/openerp/fields.py b/openerp/fields.py index 90bc3a8894870a01759c4ac00655af7030380b15..6a750850721b1088020af1fa12bb10ef7f7bf955 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -884,7 +884,7 @@ class Field(object): """ Determine the value of `self` for `record`. """ env = record.env - if self.column and not (self.depends and env.in_draft): + if self.column and not (self.depends and env.in_onchange): # this is a stored field or an old-style function field if self.depends: # this is a stored computed field, check for recomputation @@ -910,7 +910,7 @@ class Field(object): elif self.compute: # this is either a non-stored computed field, or a stored computed - # field in draft mode + # field in onchange mode if self.recursive: self.compute_value(record) else: @@ -1053,7 +1053,7 @@ class Float(Field): @property def digits(self): if callable(self._digits): - with registry().cursor() as cr: + with fields._get_cursor() as cr: return self._digits(cr) else: return self._digits @@ -1280,17 +1280,16 @@ class Datetime(Field): """ assert isinstance(timestamp, datetime), 'Datetime instance expected' tz_name = record._context.get('tz') or record.env.user.tz + utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST if tz_name: try: - utc = pytz.timezone('UTC') context_tz = pytz.timezone(tz_name) - utc_timestamp = utc.localize(timestamp, is_dst=False) # UTC = no DST return utc_timestamp.astimezone(context_tz) except Exception: _logger.debug("failed to compute context/client-specific timestamp, " "using the UTC value", exc_info=True) - return timestamp + return utc_timestamp @staticmethod def from_string(value): diff --git a/openerp/http.py b/openerp/http.py index a12f912af7bf7ffbec4e6f3398c68e4e72156e02..a315a6faf02d63c39b744758a0d0e029ba83affe 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -203,7 +203,11 @@ class WebRequest(object): def env(self): """ The :class:`~openerp.api.Environment` bound to current request. + Raises a :class:`RuntimeError` if the current requests is not bound + to a database. """ + if not self.db: + return RuntimeError('request not bound to a database') return openerp.api.Environment(self.cr, self.uid, self.context) @lazy_property @@ -238,6 +242,8 @@ class WebRequest(object): """ # can not be a lazy_property because manual rollback in _call_function # if already set (?) + if not self.db: + return RuntimeError('request not bound to a database') if not self._cr: self._cr = self.registry.cursor() return self._cr @@ -907,7 +913,8 @@ class Model(object): raise Exception("Access denied") mod = request.registry[self.model] meth = getattr(mod, method) - cr = request.cr + # make sure to instantiate an environment + cr = request.env.cr result = meth(cr, request.uid, *args, **kw) # reorder read if method == "read": @@ -923,6 +930,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): def __init__(self, *args, **kwargs): self.inited = False self.modified = False + self.rotate = False super(OpenERPSession, self).__init__(*args, **kwargs) self.inited = True self._default_values() @@ -958,6 +966,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): uid = dispatch_rpc('common', 'authenticate', [db, login, password, env]) else: security.check(db, uid, password) + self.rotate = True self.db = db self.uid = uid self.login = login @@ -983,6 +992,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session): if not (keep_db and k == 'db'): del self[k] self._default_values() + self.rotate = True def _default_values(self): self.setdefault("db", None) @@ -1378,7 +1388,22 @@ class Root(object): else: response = result + # save to cache if requested and possible + if getattr(request, 'cache_save', False) and response.status_code == 200: + response.freeze() + r = response.response + if isinstance(r, list) and len(r) == 1 and isinstance(r[0], str): + request.registry.cache[request.cache_save] = { + 'content': r[0], + 'mimetype': response.headers['Content-Type'], + 'time': time.time(), + } + if httprequest.session.should_save: + if httprequest.session.rotate: + self.session_store.delete(httprequest.session) + httprequest.session.sid = self.session_store.generate_key() + httprequest.session.modified = True self.session_store.save(httprequest.session) # We must not set the cookie if the session id was specified using a http header or a GET parameter. # There are two reasons to this: diff --git a/openerp/models.py b/openerp/models.py index 1dc6dcb70d1af95c464b8682e0229ce4a68eed41..17f14643c42bcba9112371c0421de5ef8c5cda28 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -73,6 +73,7 @@ from .tools.translate import _ _logger = logging.getLogger(__name__) _schema = logging.getLogger(__name__ + '.schema') +_unlink = logging.getLogger(__name__ + '.unlink') regex_order = re.compile('^( *([a-z0-9:_]+|"[a-z0-9:_]+")( *desc| *asc)?( *, *|))+$', re.I) regex_object_name = re.compile(r'^[a-z0-9_.]+$') @@ -1088,6 +1089,13 @@ class BaseModel(object): ids = False return {'ids': ids, 'messages': messages} + def _add_fake_fields(self, cr, uid, fields, context=None): + from openerp.fields import Char, Integer + fields[None] = Char('rec_name') + fields['id'] = Char('External ID') + fields['.id'] = Integer('Database ID') + return fields + def _extract_records(self, cr, uid, fields_, data, context=None, log=lambda a: None): """ Generates record dicts from the data sequence. @@ -1103,13 +1111,9 @@ class BaseModel(object): * "id" is the External ID for the record * ".id" is the Database ID for the record """ - from openerp.fields import Char, Integer fields = dict(self._fields) # Fake fields to avoid special cases in extractor - fields[None] = Char('rec_name') - fields['id'] = Char('External ID') - fields['.id'] = Integer('Database ID') - + fields = self._add_fake_fields(cr, uid, fields, context=context) # m2o fields can't be on multiple lines so exclude them from the # is_relational field rows filter, but special-case it later on to # be handled with relational fields (as it can have subfields) @@ -1808,7 +1812,7 @@ class BaseModel(object): ``tools.ormcache`` or ``tools.ormcache_multi``. """ try: - self.pool.cache.clear_prefix((self._name,)) + self.pool.cache.clear() self.pool._any_cache_cleared = True except AttributeError: pass @@ -2174,14 +2178,20 @@ class BaseModel(object): :param query: query object on which the JOIN should be added :return: qualified name of field, to be used in SELECT clause """ - current_table = self - parent_alias = '"%s"' % current_table._table - while field in current_table._inherit_fields and not field in current_table._columns: - parent_model_name = current_table._inherit_fields[field][0] - parent_table = self.pool[parent_model_name] - parent_alias = self._inherits_join_add(current_table, parent_model_name, query) - current_table = parent_table - return '%s."%s"' % (parent_alias, field) + # INVARIANT: alias is the SQL alias of model._table in query + model, alias = self, self._table + while field in model._inherit_fields and field not in model._columns: + # retrieve the parent model where field is inherited from + parent_model_name = model._inherit_fields[field][0] + parent_model = self.pool[parent_model_name] + parent_field = model._inherits[parent_model_name] + # JOIN parent_model._table AS parent_alias ON alias.parent_field = parent_alias.id + parent_alias, _ = query.add_join( + (alias, parent_model._table, parent_field, 'id', parent_field), + implicit=True, + ) + model, alias = parent_model, parent_alias + return '"%s"."%s"' % (alias, field) def _parent_store_compute(self, cr): if not self._parent_store: @@ -2428,6 +2438,11 @@ class BaseModel(object): # has not been added in database yet! context = dict(context or {}, prefetch_fields=False) + # Make sure an environment is available for get_pg_type(). This is + # because we access column.digits, which retrieves a cursor from + # existing environments. + env = api.Environment(cr, SUPERUSER_ID, context) + store_compute = False stored_fields = [] # new-style stored fields with compute todo_end = [] @@ -3197,7 +3212,7 @@ class BaseModel(object): records = records[:PREFETCH_MAX] | self # determine which fields can be prefetched - if not self.env.in_draft and \ + if not self.env.in_onchange and \ self._context.get('prefetch_fields', True) and \ self._columns[field.name]._prefetch: # prefetch all classic and many2one fields that the user can access @@ -3652,6 +3667,9 @@ class BaseModel(object): # recompute new-style fields recs.recompute() + # auditing: deletions are infrequent and leave no trace in the database + _unlink.info('User #%s deleted %s records with IDs: %r', uid, self._name, ids) + return True # @@ -3993,10 +4011,10 @@ class BaseModel(object): recs.invalidate_cache(['parent_left', 'parent_right']) result += self._store_get_values(cr, user, ids, vals.keys(), context) - result.sort() done = {} - for order, model_name, ids_to_update, fields_to_recompute in result: + recs.env.recompute_old.extend(result) + for order, model_name, ids_to_update, fields_to_recompute in sorted(recs.env.recompute_old): key = (model_name, tuple(fields_to_recompute)) done.setdefault(key, {}) # avoid to do several times the same computation @@ -4007,6 +4025,7 @@ class BaseModel(object): if id not in deleted_related[model_name]: todo.append(id) self.pool[model_name]._store_set_values(cr, user, todo, fields_to_recompute, context) + recs.env.clear_recompute_old() # recompute new-style fields if recs.env.recompute and context.get('recompute', True): @@ -4256,16 +4275,18 @@ class BaseModel(object): # check Python constraints recs._validate_fields(vals) - if recs.env.recompute and context.get('recompute', True): - result += self._store_get_values(cr, user, [id_new], + result += self._store_get_values(cr, user, [id_new], list(set(vals.keys() + self._inherits.values())), context) - result.sort() + recs.env.recompute_old.extend(result) + + if recs.env.recompute and context.get('recompute', True): done = [] - for order, model_name, ids, fields2 in result: + for order, model_name, ids, fields2 in sorted(recs.env.recompute_old): if not (model_name, ids, fields2) in done: self.pool[model_name]._store_set_values(cr, user, ids, fields2, context) done.append((model_name, ids, fields2)) + recs.env.clear_recompute_old() # recompute new-style fields recs.recompute() @@ -5666,7 +5687,11 @@ class BaseModel(object): while self.env.has_todo(): field, recs = self.env.get_todo() # evaluate the fields to recompute, and save them to database - names = [f.name for f in field.computed_fields if f.store] + names = [ + f.name + for f in field.computed_fields + if f.store and self.env.field_todo(f) + ] for rec in recs: try: values = rec._convert_to_write({ diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index a6d3e41c0dbfd5121704f00658304ce50a9eda8b..cf45836ef707409abee44c9b2410c57147e95cc3 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -173,6 +173,8 @@ class Registry(Mapping): :param partial: ``True`` if all models have not been loaded yet. """ + lazy_property.reset_all(self) + # load custom models ir_model = self['ir.model'] cr.execute('select model from ir_model where state=%s', ('manual',)) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index f890d23a5bda75086600e2f70ce8322bc47335de..8f220304a7f9b5f86bd8733b36e2bfe19ffd6930 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -22,6 +22,7 @@ import logging import logging.handlers import os +import platform import pprint import release import sys @@ -77,10 +78,11 @@ class PostgreSQLHandler(logging.Handler): def emit(self, record): ct = threading.current_thread() ct_db = getattr(ct, 'dbname', None) - dbname = tools.config['log_db'] or ct_db + dbname = tools.config['log_db'] if tools.config['log_db'] and tools.config['log_db'] != '%d' else ct_db if not dbname: return with tools.ignore(Exception), tools.mute_logger('openerp.sql_db'), sql_db.db_connect(dbname, allow_uri=True).cursor() as cr: + cr.autocommit(True) msg = tools.ustr(record.msg) if record.args: msg = msg % record.args @@ -144,8 +146,10 @@ def init_logger(): # SysLog Handler if os.name == 'nt': handler = logging.handlers.NTEventLogHandler("%s %s" % (release.description, release.version)) + elif platform.system() == 'Darwin': + handler = logging.handlers.SysLogHandler('/var/run/log') else: - handler = logging.handlers.SysLogHandler() + handler = logging.handlers.SysLogHandler('/dev/log') format = '%s %s' % (release.description, release.version) \ + ':%(dbname)s:%(levelname)s:%(name)s:%(message)s' @@ -182,8 +186,15 @@ def init_logger(): logging.getLogger().addHandler(handler) if tools.config['log_db']: + db_levels = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL, + } postgresqlHandler = PostgreSQLHandler() - postgresqlHandler.setLevel(25) + postgresqlHandler.setLevel(int(db_levels.get(tools.config['log_db_level'], tools.config['log_db_level']))) logging.getLogger().addHandler(postgresqlHandler) # Configure loggers levels diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 0dbe417b8149798ee342b3115d7328b124c3231e..496e7b9dc940404e9689a73175d8ded3500b5ed5 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -401,7 +401,7 @@ def is_leaf(element, internal=False): and len(element) == 3 \ and element[1] in INTERNAL_OPS \ and ((isinstance(element[0], basestring) and element[0]) - or element in (TRUE_LEAF, FALSE_LEAF)) + or tuple(element) in (TRUE_LEAF, FALSE_LEAF)) # -------------------------------------------------- diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 32ed1c510fabef3fcac65c3102de6a5a936ea164..d94036e50754fa9bde4d923e3def69c297a4d36b 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -43,6 +43,7 @@ import pytz import re import xmlrpclib from operator import itemgetter +from contextlib import contextmanager from psycopg2 import Binary import openerp @@ -52,6 +53,23 @@ from openerp.tools import float_repr, float_round, frozendict, html_sanitize import simplejson from openerp import SUPERUSER_ID, registry +@contextmanager +def _get_cursor(): + # yield a valid cursor from any environment or create a new one if none found + from openerp.api import Environment + from openerp.http import request + try: + request.env # force request's env to be computed + except RuntimeError: + pass # ignore if not in a request + for env in Environment.envs: + if not env.cr.closed: + yield env.cr + break + else: + with registry().cursor() as cr: + yield cr + EMPTY_DICT = frozendict() _logger = logging.getLogger(__name__) @@ -151,6 +169,8 @@ class _column(object): def __getattr__(self, name): """ Access a non-slot attribute. """ + if name == '_args': + raise AttributeError(name) try: return self._args[name] except KeyError: @@ -386,7 +406,7 @@ class float(_column): @property def digits(self): if self._digits_compute: - with registry().cursor() as cr: + with _get_cursor() as cr: return self._digits_compute(cr) else: return self._digits @@ -1316,7 +1336,7 @@ class function(_column): @property def digits(self): if self._digits_compute: - with registry().cursor() as cr: + with _get_cursor() as cr: return self._digits_compute(cr) else: return self._digits diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index db1da378bbe108d83f725fe71d9db528720de3e0..835e29da7f910b40eca102508fda373506ca01a8 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -11,6 +11,8 @@ from ..models import ( LOG_ACCESS_COLUMNS, ) +from openerp.tools.safe_eval import safe_eval as eval + # extra definitions for backward compatibility browse_record_list = BaseModel diff --git a/openerp/report/report_sxw.py b/openerp/report/report_sxw.py index 5a5754fe5525a489f1a6b849dbb8cfe1a5b609e9..8d9fbbd6aedb798e56839e80d53e95659ae292bc 100644 --- a/openerp/report/report_sxw.py +++ b/openerp/report/report_sxw.py @@ -39,6 +39,7 @@ from openerp import SUPERUSER_ID from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field from openerp.tools.translate import _ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.safe_eval import safe_eval as eval _logger = logging.getLogger(__name__) @@ -242,9 +243,9 @@ class rml_parse(object): res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) if currency_obj: if currency_obj.position == 'after': - res='%s %s'%(res,currency_obj.symbol) + res = u'%s\N{NO-BREAK SPACE}%s' % (res, currency_obj.symbol) elif currency_obj and currency_obj.position == 'before': - res='%s %s'%(currency_obj.symbol, res) + res = u'%s\N{NO-BREAK SPACE}%s' % (currency_obj.symbol, res) return res def display_address(self, address_record, without_company=False): diff --git a/openerp/service/server.py b/openerp/service/server.py index a199951d717134d94ace0257c57ffb0c4391596b..524d27e4eabdcb251ce3927d6a5e42553412bf2f 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -167,6 +167,10 @@ class CommonServer(object): try: sock.shutdown(socket.SHUT_RDWR) except socket.error, e: + if e.errno == errno.EBADF: + # Werkzeug > 0.9.6 closes the socket itself (see commit + # https://github.com/mitsuhiko/werkzeug/commit/4d8ca089) + return # On OSX, socket shutdowns both sides if any side closes it # causing an error 57 'Socket is not connected' on shutdown # of the other side (or something), see diff --git a/openerp/sql_db.py b/openerp/sql_db.py index fd89d857019e81e792bf4379b7781a2907954185..33e23648992c44d1467b16511d47f44377ba4a5b 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -265,7 +265,7 @@ class Cursor(object): def split_for_in_conditions(self, ids): """Split a list of identifiers into one or more smaller tuples safe for IN conditions, after uniquifying them.""" - return tools.misc.split_every(self.IN_MAX, set(ids)) + return tools.misc.split_every(self.IN_MAX, ids) def print_log(self): global sql_counter @@ -402,6 +402,10 @@ class Cursor(object): def __getattr__(self, name): return getattr(self._obj, name) + @property + def closed(self): + return self._closed + class TestCursor(Cursor): """ A cursor to be used for tests. It keeps the transaction open across several requests, and simulates committing, rolling back, and closing. diff --git a/openerp/tools/cache.py b/openerp/tools/cache.py index 339c942f3c7aba6c11b5b942fd63740ab4f412fc..1a8d36b3a26a7e4a01488337e6d1b43fa23145dc 100644 --- a/openerp/tools/cache.py +++ b/openerp/tools/cache.py @@ -107,13 +107,9 @@ class ormcache(object): return self.method(*args, **kwargs) def clear(self, model, *args): - """ Remove *args entry from the cache or all keys if *args is undefined """ + """ Clear the registry cache """ d, key0, _ = self.lru(model) - if args: - _logger.warn("ormcache.clear arguments are deprecated and ignored " - "(while clearing caches on (%s).%s)", - model._name, self.method.__name__) - d.clear_prefix(key0) + d.clear() model.pool._any_cache_cleared = True diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 1e554071f4922160232de5bf4c4774bf3a65a939..cd765aeae4e67d2720d71286c85b70eb64a7fd71 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -179,6 +179,7 @@ class configmanager(object): group.add_option('--log-web', action="append_const", dest="log_handler", const="openerp.http:DEBUG", help='shortcut for --log-handler=openerp.http:DEBUG') group.add_option('--log-sql', action="append_const", dest="log_handler", const="openerp.sql_db:DEBUG", help='shortcut for --log-handler=openerp.sql_db:DEBUG') group.add_option('--log-db', dest='log_db', help="Logging database", my_default=False) + group.add_option('--log-db-level', dest='log_db_level', my_default='warning', help="Logging database level") # For backward-compatibility, map the old log levels to something # quite close. levels = [ @@ -383,7 +384,7 @@ class configmanager(object): 'db_maxconn', 'import_partial', 'addons_path', 'xmlrpc', 'syslog', 'without_demo', 'dbfilter', 'log_level', 'log_db', - 'geoip_database', + 'log_db_level', 'geoip_database', ] for arg in keys: diff --git a/openerp/tools/image.py b/openerp/tools/image.py index 9ed960f37a2a24af50b5afacbb9761a43f907cb1..f860ab4d5475740908a5894cb0349f36a519ec93 100644 --- a/openerp/tools/image.py +++ b/openerp/tools/image.py @@ -130,8 +130,15 @@ def image_save_for_web(image, fp=None, format=None): if image.format == 'PNG': opt.update(optimize=True) if image.mode != 'P': + # Get the alpha band + alpha = image.split()[-1] # Floyd Steinberg dithering by default image = image.convert('RGBA').convert('P', palette=Image.WEB, colors=256) + # Set all pixel values below 128 to 255 and the rest to 0 + mask = Image.eval(alpha, lambda a: 255 if a <=128 else 0) + # Paste the color of index 255 and use alpha as a mask + image.paste(255, mask) + opt.update(transparency=255) elif image.format == 'JPEG': opt.update(optimize=True, quality=80) if fp: diff --git a/openerp/tools/lru.py b/openerp/tools/lru.py index d0e163b28614a1bb2cacfda70c4c6461d1c4b2fc..9d1b5f685ff26526797a992baf89a946661ed5c4 100644 --- a/openerp/tools/lru.py +++ b/openerp/tools/lru.py @@ -118,11 +118,3 @@ class LRU(object): self.d = {} self.first = None self.last = None - - @synchronized() - def clear_prefix(self, prefix): - """ Remove from `self` all the items with the given `prefix`. """ - n = len(prefix) - for key in self.keys(): - if key[:n] == prefix: - del self[key] diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index cb03ee473e314e8ac81ecfe5d547e9088165e8b8..d13df2c0d2012c46bdeb3b4d202100df65a7a7b0 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -511,6 +511,7 @@ ALL_LANGUAGES = { 'it_IT': u'Italian / Italiano', 'iu_CA': u'Inuktitut / áƒá“„ᒃᑎá‘ᑦ', 'ja_JP': u'Japanese / 日本語', + 'kab_DZ': u'Kabyle / Taqbaylit', 'ko_KP': u'Korean (KP) / í•œêµì–´ (KP)', 'ko_KR': u'Korean (KR) / í•œêµì–´ (KR)', 'lo_LA': u'Lao / ພາສາລາວ',