diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index fadd4a6028c..feaba47626b 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -483,9 +483,9 @@ class AccountInvoice(models.Model): else: account_id = pay_account.id payment_term_id = p.property_supplier_payment_term_id.id - addr = self.partner_id.address_get(['delivery']) - fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=addr['delivery']) + delivery_partner_id = self.get_delivery_partner_id() + fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=delivery_partner_id) # If partner has no warning, check its company if p.invoice_warn == 'no-message' and p.parent_id: @@ -513,6 +513,10 @@ class AccountInvoice(models.Model): return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}} return {} + @api.multi + def get_delivery_partner_id(self): + self.ensure_one() + return self.partner_id.address_get(['delivery'])['delivery'] @api.onchange('journal_id') def _onchange_journal_id(self): @@ -1030,7 +1034,7 @@ class AccountInvoice(models.Model): values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids) - tax_lines = filter(lambda l: l.manual, invoice.tax_line_ids) + tax_lines = invoice.tax_line_ids values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines) if journal_id: diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index e0df2075a29..62a760aa017 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -304,11 +304,13 @@ class account_journal(models.Model): 'default_type': invoice_type, 'type': invoice_type }) + [action] = self.env.ref('account.%s' % action_name).read() action['context'] = ctx action['domain'] = self._context.get('use_domain', []) - action['views'] = False - action['view_id'] = False + if action_name in ['action_bank_statement_tree', 'action_view_bank_statement_tree']: + action['views'] = False + action['view_id'] = False return action @api.multi diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index d922e3b7410..c7bf678a2d8 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -8,6 +8,7 @@ from odoo.exceptions import RedirectWarning, UserError, ValidationError from odoo.tools.misc import formatLang from odoo.tools import float_is_zero, float_compare from odoo.tools.safe_eval import safe_eval +import odoo.addons.decimal_precision as dp from lxml import etree #---------------------------------------------------------- @@ -370,7 +371,7 @@ class AccountMoveLine(models.Model): self.counterpart = ",".join(counterpart) name = fields.Char(required=True, string="Label") - quantity = fields.Float(digits=(16, 2), + quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'), help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports.") product_uom_id = fields.Many2one('product.uom', string='Unit of Measure') product_id = fields.Many2one('product.product', string='Product') @@ -763,7 +764,7 @@ class AccountMoveLine(models.Model): """ for datum in data: if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2: - self.process_reconciliation(datum['mv_line_ids'], datum['new_mv_line_dicts']) + self.env['account.move.line'].browse(datum['mv_line_ids']).process_reconciliation(datum['new_mv_line_dicts']) if datum['type'] == 'partner': partners = self.env['res.partner'].browse(datum['id']) @@ -1426,9 +1427,9 @@ class AccountPartialReconcile(models.Model): for rec in self: if not rec.company_id.currency_exchange_journal_id: raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not self.company_id.income_currency_exchange_account_id.id: + if not rec.company_id.income_currency_exchange_account_id.id: raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not self.company_id.expense_currency_exchange_account_id.id: + if not rec.company_id.expense_currency_exchange_account_id.id: raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id} @@ -1488,6 +1489,8 @@ class AccountPartialReconcile(models.Model): #amount is the current cash_basis amount minus the one before the reconciliation amount = line.balance * percentage_after - line.balance * percentage_before rounded_amt = line.company_id.currency_id.round(amount) + if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): + continue if line.tax_line_id and line.tax_line_id.use_cash_basis: if not newly_created_move: newly_created_move = self._create_tax_basis_move() diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index c01d22cac04..ba2503d41a9 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -11,6 +11,7 @@ class ReportPartnerLedger(models.AbstractModel): def _lines(self, data, partner): full_account = [] + currency = self.env['res.currency'] query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] @@ -40,6 +41,7 @@ class ReportPartnerLedger(models.AbstractModel): ) sum += r['debit'] - r['credit'] r['progress'] = sum + r['currency_id'] = currency.browse(r.get('currency_id')) full_account.append(r) return full_account diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index f6b11825a1e..fafcb833205 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -301,10 +301,10 @@ - customersCustomers - suppliersVendors - othersOtherAccounts - allAll + Customers + Vendors + OtherAccounts + All diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 0a0ec39363d..1b1ec7e2799 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -638,7 +638,7 @@ - + form diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 9809cf93e54..72e71cde309 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -126,18 +126,23 @@ -
+ +
+ + diff --git a/addons/account/views/report_partnerledger.xml b/addons/account/views/report_partnerledger.xml index 0259e5167e8..5294736086c 100644 --- a/addons/account/views/report_partnerledger.xml +++ b/addons/account/views/report_partnerledger.xml @@ -36,7 +36,7 @@ - + @@ -77,8 +77,10 @@ - diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index c0c2a0a14f5..bb30c799611 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -58,7 +58,6 @@ class AccountInvoiceRefund(models.TransientModel): date = form.date or False description = form.description or inv.name refund = inv.refund(form.date_invoice, date, description, inv.journal_id.id) - refund.compute_taxes() created_inv.append(refund.id) if mode in ('cancel', 'modify'): diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index 6f1251ccaca..83639b5f3a5 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -471,15 +471,14 @@ class AccountAssetDepreciationLine(models.Model): @api.multi def create_move(self, post_move=True): created_moves = self.env['account.move'] + prec = self.env['decimal.precision'].precision_get('Account') for line in self: category_id = line.asset_id.category_id 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 = current_currency.compute(line.amount, company_currency) - sign = (category_id.journal_id.type == 'purchase' or category_id.journal_id.type == 'sale' and 1) or -1 asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) - prec = self.env['decimal.precision'].precision_get('Account') move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, @@ -489,7 +488,7 @@ class AccountAssetDepreciationLine(models.Model): 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, - 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0, + 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0, } move_line_2 = { 'name': asset_name, @@ -500,7 +499,7 @@ class AccountAssetDepreciationLine(models.Model): 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, - 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0, + 'amount_currency': company_currency != current_currency and line.amount or 0.0, } move_vals = { 'ref': line.asset_id.code, diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index da443656d12..e73f009b5d6 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -61,6 +61,9 @@ widgets.SidebarFilter.include({ }, initialize_m2o: function() { this.dfm = new form_common.DefaultFieldManager(this); + if (!this.view.useContacts) { + return; + } this.dfm.extend_field_desc({ partner_id: { relation: "res.partner", diff --git a/addons/event/models/event.py b/addons/event/models/event.py index d6892e927af..03aa47bbb27 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -70,7 +70,7 @@ class EventEvent(models.Model): 'interval_unit': 'days', 'interval_type': 'before_event', 'template_id': self.env.ref('event.event_reminder') - })] + })] if self.user_has_groups('event.group_email_scheduling') else [] # Seats and computation seats_max = fields.Integer( diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 6a5edd12854..70d0a728fd4 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -49,7 +49,7 @@ class EventMailScheduler(models.Model): if self.interval_type in ['before_event', 'after_event']: self.done = self.mail_sent else: - self.done = len(self.mail_registration_ids) == len(self.event_id.registration_ids) and all(filter(lambda line: line.mail_sent, self.mail_registration_ids)) + self.done = len(self.mail_registration_ids) == len(self.event_id.registration_ids) and all(line.mail_sent for line in self.mail_registration_ids) @api.one @api.depends('event_id.state', 'event_id.date_begin', 'interval_type', 'interval_unit', 'interval_nbr') diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index 70a9f8049e6..b4a66363ada 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -434,8 +434,8 @@ - - + + diff --git a/addons/fleet/models/fleet.py b/addons/fleet/models/fleet.py index 7ff6f95dc5d..364b44fff16 100644 --- a/addons/fleet/models/fleet.py +++ b/addons/fleet/models/fleet.py @@ -213,11 +213,11 @@ class FleetVehicle(models.Model): LogContract = self.env['fleet.vehicle.log.contract'] Cost = self.env['fleet.vehicle.cost'] for record in self: - record.odometer_count = Odometer.search_count([('vehicle_id', '=', self.id)]) - record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', self.id)]) - record.service_count = LogService.search_count([('vehicle_id', '=', self.id)]) - record.contract_count = LogContract.search_count([('vehicle_id', '=', self.id)]) - record.cost_count = Cost.search_count([('vehicle_id', '=', self.id), ('parent_id', '=', False)]) + record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)]) + record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)]) + record.service_count = LogService.search_count([('vehicle_id', '=', record.id)]) + record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id)]) + record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)]) @api.depends('log_contracts') def _compute_contract_reminder(self): diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 95d4ad23324..8232084123b 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -246,33 +246,40 @@ class HrExpense(models.Model): move.post() return True + @api.multi + def _prepare_move_line_value(self): + self.ensure_one() + if self.account_id: + account = self.account_id + elif self.product_id: + account = self.product_id.product_tmpl_id._get_product_accounts()['expense'] + if not account: + raise UserError( + _("No Expense account found for the product %s (or for its category), please configure one.") % (self.product_id.name)) + else: + account = self.env['ir.property'].with_context(force_company=self.company_id.id).get('property_account_expense_categ_id', 'product.category') + if not account: + raise UserError( + _('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.')) + aml_name = self.employee_id.name + ': ' + self.name.split('\n')[0][:64] + move_line = { + 'type': 'src', + 'name': aml_name, + 'price_unit': self.unit_amount, + 'quantity': self.quantity, + 'price': self.total_amount, + 'account_id': account.id, + 'product_id': self.product_id.id, + 'uom_id': self.product_uom_id.id, + 'analytic_account_id': self.analytic_account_id.id, + } + return move_line + @api.multi def _move_line_get(self): account_move = [] for expense in self: - if expense.account_id: - account = expense.account_id - elif expense.product_id: - account = expense.product_id.product_tmpl_id._get_product_accounts()['expense'] - if not account: - raise UserError(_("No Expense account found for the product %s (or for its category), please configure one.") % (expense.product_id.name)) - else: - account = self.env['ir.property'].with_context(force_company=expense.company_id.id).get('property_account_expense_categ_id', 'product.category') - if not account: - raise UserError(_('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.')) - - aml_name = expense.employee_id.name + ': ' + expense.name.split('\n')[0][:64] - move_line = { - 'type': 'src', - 'name': aml_name, - 'price_unit': expense.unit_amount, - 'quantity': expense.quantity, - 'price': expense.total_amount, - 'account_id': account.id, - 'product_id': expense.product_id.id, - 'uom_id': expense.product_uom_id.id, - 'analytic_account_id': expense.analytic_account_id.id, - } + move_line = expense._prepare_move_line_value() account_move.append(move_line) # Calculate tax lines and adjust base line diff --git a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py index edb11786749..49adf3fed7f 100644 --- a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py @@ -10,7 +10,7 @@ class HrTimesheetSheet(models.Model): _inherit = "hr_timesheet_sheet.sheet" attendances_ids = fields.One2many('hr.attendance', 'sheet_id', 'Attendances') - total_attendance = fields.Integer(string='Total Attendance', compute='_compute_total') + total_attendance = fields.Float(string='Total Attendance', compute='_compute_total') total_timesheet = fields.Float(string='Total Timesheet', compute="_compute_total") total_difference = fields.Float(string='Difference', compute="_compute_total") period_ids = fields.One2many('hr_timesheet_sheet.sheet.day', 'sheet_id', string='Period', readonly=True) diff --git a/addons/l10n_at/data/l10n_at_chart_data.xml b/addons/l10n_at/data/l10n_at_chart_data.xml index 8bc33d795cf..b280c11c0e3 100644 --- a/addons/l10n_at/data/l10n_at_chart_data.xml +++ b/addons/l10n_at/data/l10n_at_chart_data.xml @@ -779,7 +779,7 @@ 2500 - + Vorsteuer diff --git a/addons/l10n_in_schedule6/__init__.py b/addons/l10n_in_schedule6/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/l10n_in_schedule6/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/l10n_in_schedule6/__openerp__.py b/addons/l10n_in_schedule6/__openerp__.py new file mode 100644 index 00000000000..20b803525d1 --- /dev/null +++ b/addons/l10n_in_schedule6/__openerp__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Indian - Schedule VI Accounting', + 'version': '2.0', + 'description': """ +Indian Accounting: Chart of Account. +==================================== + +Indian accounting chart and localization. + +Schedule VI chart of account for indian accounting. + +Note: The Schedule VI has been revised by MCA and is applicable for all Balance Sheet made after +31st March, 2011. The Format has done away with earlier two options of format of Balance +Sheet, now only Vertical format has been permitted Which is Supported By OpenERP. + """, + 'category': 'Localization', + 'depends': [ + 'account', + ], + 'demo': [], + 'data': [ + 'l10n_in_schedule6_chart.xml', + 'l10n_in_schedule6_tax_template.xml', + 'account_chart_template.yml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/addons/l10n_in_schedule6/account_chart_template.yml b/addons/l10n_in_schedule6/account_chart_template.yml new file mode 100644 index 00000000000..e536be4873d --- /dev/null +++ b/addons/l10n_in_schedule6/account_chart_template.yml @@ -0,0 +1,3 @@ +- + !python {model: account.chart.template, id: indian_chart_template_schedule6}: | + self[0].try_loading_for_current_company() diff --git a/addons/l10n_in_schedule6/l10n_in_schedule6_chart.xml b/addons/l10n_in_schedule6/l10n_in_schedule6_chart.xml new file mode 100644 index 00000000000..5552803bd7a --- /dev/null +++ b/addons/l10n_in_schedule6/l10n_in_schedule6_chart.xml @@ -0,0 +1,1433 @@ + + + + + + 1206 + Transfer Account + + + + + Indian Chart of Accounts - Schedule VI + 12142 + 12141 + 6 + + + + + + + + + + + Authorised Share Capital + 11101 + + + + + + Issued, Subscribed And Paid Up Capital + 11102 + + + + + + Capital Reserve + 11110 + + + + + + Capital Redemption Reserve + 11111 + + + + + + Securities Premium Reserve + 11112 + + + + + + Debenture Redeemption Reserve + 11113 + + + + + + Revaluation Reserve + 11114 + + + + + + Shares Option Outstanding Account + 11115 + + + + + + Other Reserve + 11116 + + + + + + Surplus (Profit and Loss Account) + 11117 + + + + + + Money Received Against Share Warrants + 1112 + + + + + + + + Share Application Money Pending Allotment + 1121 + + + + + + + Bank Loan (Long Term) + 113011 + + + + + + From Others + 113021 + + + + + + Deferred Tax Liabilities (Net) + 1131 + + + + + + Other Long Term Liabilities + 1132 + + + + + + Long Term Provisions + 1133 + + + + + + + + Bank Loan (Short Term) + 114011 + + + + + + Loans And Advances From Related Parties + 11402 + + + + + + Depsoits + 11403 + + + + + + Others + 11404 + + + + + + + Sundry Creditors For Material/Supplies + 11411 + + + + + + Sundry Creditors For Services + 11412 + + + + + + + Excise Duty Payable + 1142111 + + + + + + Education Cess Payable On Excise Duty + 1142112 + + + + + + Higher Education Cess Payable On Excise Duty + 1142113 + + + + + + Service Tax Payable + 1142121 + + + + + + Education Cess Payable On Service Tax + 1142122 + + + + + + Higher Education Cess Payable On Service Tax + 1142123 + + + + + + TDS Paybale + 114213 + + + + + + Central Sales Tax Payable + 114214 + + + + + + VAT Payable + 114215 + + + + + + Out Standing Cheques for Clearance + 11422 + + + + + + Advance From Customers + 11423 + + + + + + + EPF Payable + 114301 + + + + + + Unpaid Remuneration + 114302 + + + + + + Electricity And Power Charges Payable + 114311 + + + + + + Employee Providend Fund Payable + 114312 + + + + + + Internet Expenses Payable + 114313 + + + + + + Mobile Expenses Payable + 114314 + + + + + + Secueity Expenses Payable + 114315 + + + + + + Telephone Expenses Payable + 114316 + + + + + + Wages Payable + 114317 + + + + + + Provision For Taxation + 114318 + + + + + + Provision For Expenses + 114319 + + + + + + + + + Land + 120101 + + + + + + Buildings + 120102 + + + + + + Plant And Equipments + 120103 + + + + + + Furnitures And Fixtures + 120104 + + + + + + Vehicles + 120105 + + + + + + Computer/Laptops + 120106 + + + + + + Office Equipments + 120107 + + + + + + + Goodwill + 120111 + + + + + + + Building Under Construction + 120120 + + + + + + + Intangible Assets Under Development + 12013 + + + + + + + Investment In Property + 12020 + + + + + + Investment In Equity Instrument + 12021 + + + + + + Investment In Preference Shares + 12022 + + + + + + Investment In Government Or Trust Securities + 12023 + + + + + + Investment In Debentures And Bonds + 12024 + + + + + + Investment In Mutual Fund + 12025 + + + + + + Investment In Partnership Firm + 12026 + + + + + + Gold + 120270 + + + + + + Fixed Deposit With Banks-Earmarked + 120271 + + + + + + + Deferred Tax Assets (Net) + 1203 + + + + + + + Secured, Considered Good + 120411 + + + + + + Unsecured, Considered Good + 120412 + + + + + + Doubtful + 120413 + + + + + + Earnest Money Deposit + 1204211 + + + + + + Other Deposit + 1204212 + + + + + + Unsecured, Considered Good + 120422 + + + + + + Doubtful + 120423 + + + + + + Loans And Advances To Related Parties + 12043 + + + + + + Other Loans And Advances + 12044 + + + + + + + Secured, Considered Good + 120511 + + + + + + Unsecured, Considered Good + 120512 + + + + + + Doubtful + 120513 + + + + + + Preliminery Expense + 120521 + + + + + + Software + 120522 + + + + + + Others + 12053 + + + + + + + + Investment In Equity + 12110 + + + + + + Investment In Preference Shares + 12111 + + + + + + Investment In Government Securities + 12112 + + + + + + Investment In Debentures And Bonds + 12113 + + + + + + Investment In Mutual Fund + 12114 + + + + + + Investment In Partnership Firm + 12115 + + + + + + In Short Term Fixed Deposits + 12116 + + + + + + + Raw Material + 12120 + + + + + + Work-In-Progress + 12121 + + + + + + Finished Goods + 12122 + + + + + + Stock-In-Trade + 12123 + + + + + + Stores And Spares + 12124 + + + + + + Loose Tools + 12125 + + + + + + Other With Job Worker + 12126 + + + + + + Goods-In-Transit + 12127 + + + + + + + Secured, Considered Good + 121311 + + + + + + Unsecured, Considered Good + 121312 + + + + + + Doubtful + 121313 + + + + + + Secured, Considered Good + 121321 + + + + + + Unsecured, Considered Good + 121322 + + + + + + Doubtful + 121323 + + + + + + Sundry Debtors + 12133 + + + + + + + + Advance To Associate Concerns + 1215111 + + + + + + Unsecured Recoverable in Cash or in Kind or for Value to be Received + 1215121 + + + + + + Doubtful + 121513 + + + + + + Advance To Suppliers + 121521 + + + + + + Advance Income Tax/Refund Due + 121522 + + + + + + Balance With Revenue Authorities (Indirect Taxes) + 121523 + + + + + + Prepaid Expenses + 121524 + + + + + + + Sales Tax Receivable + 121610 + + + + + + VAT Receivable + 121611 + + + + + + Excise Duty Receivable + 1216121 + + + + + + Education Cess Receivable On Excise Duty + 1216122 + + + + + + Higher Education Cess Receivable On Excise Duty + 1216123 + + + + + + Service Tax Receivable + 1216131 + + + + + + Education Cess Receivable On Service Tax + 1216132 + + + + + + Higher Education Cess Receivable On Service Tax + 1216133 + + + + + + TDS Receivable + 121614 + + + + + + Preliminary Expense + 121621 + + + + + + + + Central Sales(Exclusive of Excise Duty) + 2011 + + + + + + Export Sales + 2012 + + + + + + Sales of Services + 2013 + + + + + + Job Work Receipts + 2014 + + + + + + Product Sales + 2015 + + + + + + Other Operating Income + 2016 + + + + + + + Interest + 2021 + + + + + + Other Financial Services + 2022 + + + + + + Other Non Operating Income + 2023 + + + + + + + Opening Stock + 2101 + + + + + + Raw Material + 21021 + + + + + + Stores And Consumables + 21022 + + + + + + Closing Stock + 2103 + + + + + + + Purchase of Stock-In-Trade + 2111 + + + + + + + Changes In Inventories of Finished Goods, Work-In-Progress And Stock-In-Trade + 212 + + + + + + + Salaries, Bonus, PF And ESIC + 2131 + + + + + + Directors Remuneration + 2132 + + + + + + Directors Setting Fees + 2133 + + + + + + Staff Welfare Expense + 2134 + + + + + + Commision To M.D + 2135 + + + + + + Commision To Joint M.D + 2136 + + + + + + + Interest On Cash Credit Facality + 2141 + + + + + + Interest On Car Loan + 2142 + + + + + + Interest On Other Loans + 2143 + + + + + + Bank Charges + 2144 + + + + + + Bank Gurantee Charges + 2145 + + + + + + + Depreciation + 2151 + + + + + + Preliminary Expenses + 2152 + + + + + + + Jobwork Charges + 21601 + + + + + + Stores and Spares + 21602 + + + + + + Workshop Expense + 21603 + + + + + + Centeral Sales Tax + 21604 + + + + + + Machinery Repair and Maintenance + 21605 + + + + + + Freight Forwarding and Octroi Expenses (Local) + 21606 + + + + + + Electrical Repairing Expense + 21607 + + + + + + Material Testing Expense + 21608 + + + + + + Power and Fuel Charges + 21609 + + + + + + Packing Material Expense + 216091 + + + + + + Measuring Expense + 216092 + + + + + + Processing Labour Charges + 216093 + + + + + + Car Expenses (Including Insurance) + 21611 + + + + + + Travelling Expenses (Including Foreign Travelling) + 21612 + + + + + + Repair And Maintenance Building + 21613 + + + + + + Computer Maintenace + 21614 + + + + + + General Expenses + 21615 + + + + + + Internet And Website Expenses + 21616 + + + + + + Insurance Expenses + 21617 + + + + + + Telephone Expense + 21618 + + + + + + Labour Welfare Expenses + 21619 + + + + + + Legal Expenses + 216191 + + + + + + Postage And Telegram + 216192 + + + + + + Professional Charges + 216193 + + + + + + Security Expesnes + 216194 + + + + + + Stationery Expenses + 216195 + + + + + + Auditors Remuneration + 216196 + + + + + + Rent Rates And Taxes + 216197 + + + + + + Tender Fee And Testing Charges + 216198 + + + + + + Interest On Delayed Payment of Taxes + 216199 + + + + + + Excise/ Service Tax Expenses + 2161991 + + + + + + Advertisement And Publicity + 2161992 + + + + + + Donation + 2161993 + + + + + + Export Expenses + 2161994 + + + + + + Internal Audit Fee + 2161995 + + + + + + Conveyance Expense + 2161996 + + + + + + Packing and Forwarding Expense + 2161997 + + + + + + Annual Service Fees + 2161998 + + + + + + Loan Processing Fees + 2161999 + + + + + + Office Cleaning Expense + 21619991 + + + + + + Prelimenary Expense W/Off + 21619992 + + + + + + Software Expense W/Off + 21619993 + + + + + + Custom Duty + 21619994 + + + + + + Stamping Charge + 21619995 + + + + + + Post and Courrier Charges + 21619996 + + + + + + + + + Foreign Exchange Profit + 2017 + + + + + + Foreign Exchange Loss + 2170 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/l10n_in_schedule6/l10n_in_schedule6_tax_template.xml b/addons/l10n_in_schedule6/l10n_in_schedule6_tax_template.xml new file mode 100644 index 00000000000..4b98a7efafa --- /dev/null +++ b/addons/l10n_in_schedule6/l10n_in_schedule6_tax_template.xml @@ -0,0 +1,842 @@ + + + + + + + Tax to Pay + taxes + + + + C Forms + taxes + + + + C Forms Tax Collected on Sale + taxes + + + + C Forms Tax Paid on Purchase + taxes + + + + Excise Duty + taxes + + + + Excise Duty 12.36% + taxes + + + + Excise Duty Collected on Sales + taxes + + + + Education Cess 2% Excise Duty Collected on Sales + taxes + + + + Higher Education Cess 1% Excise Duty Collected on Sales + taxes + + + + Excise Duty Paid on Purchase + taxes + + + + Education Cess 2% Excise Duty Paid on Purchase + taxes + + + + Higher Education Cess 1% Excise Duty Paid on Purchase + taxes + + + + Sales Tax + taxes + + + + Sales Tax 4% + taxes + + + + Sales Tax 4% Collected on Sale + taxes + + + + Sales Tax 4% Paid on Purchase + taxes + + + + Sales Tax 12% + taxes + + + + Sales Tax 12% Collected on Sale + taxes + + + + Sales Tax 12% Paid on Purchase + taxes + + + + Sales Tax 15% + taxes + + + + Sales Tax 15% Collected on Sale + taxes + + + + Sales Tax 15% Paid on Purchase + taxes + + + + Service Tax + taxes + + + + Service Tax 12.36% + taxes + + + + Service Tax Collected on Sales of Services + taxes + + + + Education Cess 2% Service Tax Collected on Sales of Services + taxes + + + + Higher Education Cess 1% Service Tax Collected on Sales of Services + taxes + + + + Service Tax Paid on Purchase of Services + taxes + + + + Education Cess 2% Service Tax Paid on Purchase of Services + taxes + + + + Higher Education Cess 1% Service Tax Paid on Purchase of Services + taxes + + + + VAT + taxes + + + + VAT 4% + taxes + + + + VAT 4% Collected on Sale + taxes + + + + VAT 4% Paid on Purchase + taxes + + + + VAT 8% + taxes + + + + VAT 8% Collected on Sale + taxes + + + + VAT 8% Paid on Purchase + taxes + + + + VAT 10% + taxes + + + + VAT 10% Collected on Sale + taxes + + + + VAT 10% Paid on Purchase + taxes + + + + VAT 12.5% + taxes + + + + VAT 12.5% Collected on Sale + taxes + + + + VAT 12.5% Paid on Purchase + taxes + + + + Additional VAT 1% + taxes + + + + Additional VAT 1% Collected on Sale + taxes + + + + Additional VAT 1% Paid on Purchase + taxes + + + + Additional VAT 2.5% + taxes + + + + Additional VAT 2.5% Collected on Sale + taxes + + + + Additional VAT 2.5% Paid on Purchase + taxes + + + + Tax Bases + taxes + + + + Base C Forms + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Excise Duty + taxes + + + + Base Excise Duty 12.36% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Sales Tax + taxes + + + + Base Sales Tax 4% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Sales Tax 12% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Sales Tax 15% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Service Tax + taxes + + + + Base Service Tax 12.36% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT + taxes + + + + Base VAT 4% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT 8% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT 10% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT 12.5% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Additional VAT 1% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Additional VAT 2.5% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + + (schedule6)Input Sales Tax @ 15% + (schedule6)Input Sales Tax @ 15% + sale + percent + 15 + 10 + + + + + + (schedule6)Input Sales Tax @ 12% + (schedule6)Input Sales Tax @ 12% + sale + percent + 12 + 10 + + + + + + (schedule6)Input Sales Tax @ 4% + (schedule6)Input Sales Tax @ 4% + sale + percent + 4 + 10 + + + + + + (schedule6)Additional Input VAT @ 1% + (schedule6)Additional Input VAT @ 1% + sale + percent + 1 + 15 + + + + + + (schedule6)Input VAT @ 12.5% + (schedule6)Input VAT @ 12.5% + sale + percent + 12.5 + 10 + + + + + + (schedule6)Additional Input VAT @ 2.5% + (schedule6)Additional Input VAT @ 2.5% + sale + percent + 2.5 + 15 + + + + + + (schedule6)Input VAT @ 8% + (schedule6)Input VAT @ 8% + sale + percent + 8 + 10 + + + + + + (schedule6)Input VAT @ 10% + (schedule6)Input VAT @ 10% + sale + percent + 10 + 10 + + + + + + (schedule6)Input Tax 12% + (schedule6)Input Tax 12% + none + percent + 12 + 1 + + + + + + (schedule6)Input Education Cess @ 2% On Excise Duty + (schedule6)Input Education Cess @ 2% On Excise Duty + none + percent + 0.24 + 2 + + + + + + (schedule6)Input Higher Education Cess @ 1% On Excise Duty + (schedule6)Input Higher Education Cess @ 1% On Excise Duty + none + percent + 0.1224 + 3 + + + + + + (schedule6)Input Excise Duty @ 12.36% + (schedule6)Input Excise Duty @ 12.36% + sale + group + 0 + 1 + + + + + + + (schedule6)Input Service Tax 12% + (schedule6)Input Service Tax 12% + none + percent + 12 + 13 + + + + + + (schedule6)Input Education Cess @ 2% On Service Tax + (schedule6)Input Education Cess @ 2% On Service Tax + none + percent + 0.24 + 14 + + + + + + (schedule6)Input Higher Education Cess @ 1% On Service Tax + (schedule6)Input Higher Education Cess @ 1% On Service Tax + none + percent + 0.1224 + 15 + + + + + + (schedule6)Input Service Tax @ 12.36% + (schedule6)Input Service Tax @ 12.36% + sale + group + 12 + True + 8 + + + + + + + (schedule6)Input VAT @ 4% + (schedule6)Input VAT @ 4% + sale + percent + 4 + 10 + + + + + + + + (schedule6)Output Purchase Tax @ 15% + (schedule6)Output Purchase Tax @ 15% + purchase + percent + 15 + 10 + + + + + + (schedule6)Output Purchase Tax @ 12% + (schedule6)Output Purchase Tax @ 12% + purchase + percent + 12 + 10 + + + + + + (schedule6)Output Purchase Tax @ 4% + (schedule6)Output Purchase Tax @ 4% + purchase + percent + 4 + 10 + + + + + + (schedule6)Additional Output VAT @ 1% + (schedule6)Additional Output VAT @ 1% + purchase + percent + 1 + 15 + + + + + + (schedule6)Output VAT @ 12.5% + (schedule6)Output VAT @ 12.5% + purchase + percent + 12.5 + 10 + + + + + + (schedule6)Additional Output VAT @ 2.5% + (schedule6)Additional Output VAT @ 2.5% + purchase + percent + 2.5 + 15 + + + + + + (schedule6)Output VAT @ 8% + (schedule6)Output VAT @ 8% + purchase + percent + 8 + 10 + + + + + + (schedule6)Output VAT @ 10% + (schedule6)Output VAT @ 10% + purchase + percent + 10 + 10 + + + + + + (schedule6)Output Tax 12% + (schedule6)Output Tax 12% + none + percent + 12 + 2 + + + + + + (schedule6)Output Education Cess @ 2% Excise Duty + (schedule6)Output Education Cess @ 2% Excise Duty + none + percent + 0.24 + 3 + + + + + + (schedule6)Output Higher Education Cess @ 1% Excise Duty + (schedule6)Output Higher Education Cess @ 1% Excise Duty + none + percent + 0.1224 + 4 + + + + + + (schedule6)Output Excise Duty @ 12.36% + (schedule6)Output Excise Duty @ 12.36% + purchase + group + 12 + True + 1 + + + + + + + (schedule6)Output Service Tax 12% + (schedule6)Output Service Tax 12% + none + percent + 12 + 9 + + + + + + (schedule6)Output Education Cess @ 2% On Service Tax + (schedule6)Output Education Cess @ 2% On Service Tax + none + percent + 0.24 + 10 + + + + + + (schedule6)Output Higher Education Cess @ 1% On Service Tax + (schedule6)Output Higher Education Cess @ 1% On Service Tax + none + percent + 0.1224 + 11 + + + + + + (schedule6)Output Service Tax @ 12.36% + (schedule6)Output Service Tax @ 12.36% + purchase + True + group + 12 + 8 + + + + + + + (schedule6)Output VAT @ 4% + (schedule6)Output VAT @ 4% + purchase + percent + 4 + 10 + + + + + + + diff --git a/addons/l10n_in_schedule6/static/description/icon.png b/addons/l10n_in_schedule6/static/description/icon.png new file mode 100644 index 00000000000..ddaf38f8b6d Binary files /dev/null and b/addons/l10n_in_schedule6/static/description/icon.png differ diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 216aca10619..8445c0e06fe 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1690,9 +1690,10 @@ class MailThread(models.AbstractModel): return self._message_post_process_attachments(attachments, attachment_ids, {'model': attach_model, 'res_id': attach_res_id}) def _message_post_process_attachments(self, attachments, attachment_ids, message_data): - IrAttachment, parameter_attachments = self.env['ir.attachment'], self.env['ir.attachment'] + IrAttachment = self.env['ir.attachment'] m2m_attachment_ids = [] cid_mapping = {} + fname_mapping = {} if attachment_ids: filtered_attachment_ids = self.env['ir.attachment'].sudo().search([ ('res_model', '=', 'mail.compose.message'), @@ -1708,9 +1709,7 @@ class MailThread(models.AbstractModel): name, content = attachment elif len(attachment) == 3: name, content, info = attachment - if info and info.get('cid'): - cid = info['cid'] - cid_mapping[cid] = name + cid = info and info.get('cid') else: continue if isinstance(content, unicode): @@ -1718,13 +1717,16 @@ class MailThread(models.AbstractModel): data_attach = { 'name': name, 'datas': base64.b64encode(str(content)), - 'datas_fname': cid or name, + 'datas_fname': name, 'description': name, 'res_model': message_data['model'], 'res_id': message_data['res_id'], } - parameter_attachments |= IrAttachment.create(data_attach) - m2m_attachment_ids += [(4, attach.id) for attach in parameter_attachments] + new_attachment = IrAttachment.create(data_attach) + m2m_attachment_ids.append((4, new_attachment.id)) + if cid: + cid_mapping[cid] = new_attachment + fname_mapping[name] = new_attachment if cid_mapping and message_data.get('body'): root = lxml.html.fromstring(tools.ustr(message_data['body'])) @@ -1732,12 +1734,11 @@ class MailThread(models.AbstractModel): for node in root.iter('img'): if node.get('src', '').startswith('cid:'): cid = node.get('src').split('cid:')[1] - fname = cid_mapping.get(cid, node.get('data-filename', '')) - attachment = parameter_attachments.filtered(lambda attachment: attachment.datas_fname == cid) + attachment = cid_mapping.get(cid) if not attachment: - attachment = parameter_attachments.filtered(lambda attachment: attachment.datas_fname == fname) + attachment = fname_mapping.get(node.get('data-filename'), '') if attachment: - node.set('src', '/web/image/%s' % attachment.ids[0]) + node.set('src', '/web/image/%s' % attachment.id) postprocessed = True if postprocessed: body = lxml.html.tostring(root, pretty_print=False, encoding='UTF-8') diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 6be82359009..446ae2c877d 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -312,7 +312,7 @@ - + diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index 8dae482529f..3a69530db1a 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -7,8 +7,7 @@ import werkzeug.urls from odoo import api, fields, models, tools - -URL_REGEX = r'(\bhref=[\'"]([^\'"]+)[\'"])' +from openerp.addons.link_tracker.models.link_tracker import URL_REGEX class MailMail(models.Model): diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 7ecc6274636..25ef11da9a3 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -44,7 +44,12 @@ class ProductProduct(models.Model): read_group_res = self.env['mrp.bom'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id']) mapped_data = dict([(data['product_id'][0], data['product_id_count']) for data in read_group_res]) for product in self: - product.bom_count = mapped_data.get(product.id, 0) + if product.product_tmpl_id.product_variant_count == 1: + bom_count = mapped_data.get(product.id, product.product_tmpl_id.bom_count) + else: + bom_count = mapped_data.get(product.id, 0) + product.bom_count = bom_count + def _compute_mo_count(self): read_group_res = self.env['mrp.production'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id']) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index df46c2461aa..3858b47f137 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -33,7 +33,7 @@ class StockMoveLots(models.Model): def _check_lot_id(self): if self.move_id.product_id.tracking == 'serial': lots = set([]) - for move_lot in self.move_id.move_lot_ids.filtered(lambda r: not r.lot_produced_id): + for move_lot in self.move_id.active_move_lot_ids.filtered(lambda r: not r.lot_produced_id and r.lot_id): if move_lot.lot_id in lots: raise exceptions.UserError(_('You cannot use the same serial number in two different lines.')) if float_compare(move_lot.quantity_done, 1.0, precision_rounding=move_lot.product_id.uom_id.rounding) == 1: diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py index c23e810fd1f..866bde4858b 100644 --- a/addons/mrp_repair/models/mrp_repair.py +++ b/addons/mrp_repair/models/mrp_repair.py @@ -490,7 +490,10 @@ class RepairLine(models.Model): if partner and self.product_id: self.tax_id = partner.property_account_position_id.map_tax(self.product_id.taxes_id, self.product_id, partner).ids if self.product_id: - self.name = self.product_id.display_name + if partner: + self.name = self.product_id.with_context(lang=partner.lang).display_name + else: + self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id.id warning = False diff --git a/addons/mrp_repair/report/mrp_repair_reports.xml b/addons/mrp_repair/report/mrp_repair_reports.xml index db73449155d..9963f87d614 100644 --- a/addons/mrp_repair/report/mrp_repair_reports.xml +++ b/addons/mrp_repair/report/mrp_repair_reports.xml @@ -5,7 +5,7 @@ string="Quotation / Order" id="action_report_mrp_repair_order" model="mrp.repair" - name="mrp_repair.report_mrprepairorder" + name="mrp_repair.report_mrprepairorder2" file="mrp_repair.report_mrprepairorder" report_type="qweb-pdf" /> diff --git a/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml b/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml index 02c7d02e543..826fca68b73 100644 --- a/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml +++ b/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml @@ -2,9 +2,9 @@ + + diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 28cba8db301..6eef3ae80cc 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -150,7 +150,7 @@ class AuthorizeAPI(): etree.SubElement(root, "transId").text = transaction_id customer = etree.SubElement(root, "customer") etree.SubElement(customer, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8]) - etree.SubElement(customer, "email").text = partner.email + etree.SubElement(customer, "email").text = partner.email or '' response = self._authorize_request(root) res = dict() res['profile_id'] = response.find('customerProfileId').text diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index bdf35742bec..2fefb1417ef 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -32,16 +32,22 @@ class PaypalController(http.Controller): :param response str: text response, structured in the following way: STATUS\nkey1=value1\nkey2=value2...\n + or STATUS\nError message...\n :rtype tuple(str, dict) :return: tuple containing the STATUS str and the key/value pairs parsed as a dict """ lines = filter(None, response.split('\n')) status = lines.pop(0) - pdt_post = dict(line.split('=', 1) for line in lines) - # html unescape - for post in pdt_post: - pdt_post[post] = urllib.unquote_plus(pdt_post[post]).decode('utf8') + + pdt_post = {} + for line in lines: + split = line.split('=', 1) + if len(split) == 2: + pdt_post[split[0]] = urllib.unquote_plus(split[1]).decode('utf8') + else: + _logger.warning('Paypal: error processing pdt response: %s', line) + return status, pdt_post def paypal_validate_data(self, **post): diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 3c110494630..cd8a1e8e493 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -14,7 +14,10 @@ class PosController(http.Controller): @http.route('/pos/web', type='http', auth='user') def pos_web(self, debug=False, **k): # if user not logged in, log him in - pos_sessions = request.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', request.session.uid)]) + pos_sessions = request.env['pos.session'].search([ + ('state', '=', 'opened'), + ('user_id', '=', request.session.uid), + ('name', 'not like', '(RESCUE FOR')]) if not pos_sessions: return werkzeug.utils.redirect('/web#action=point_of_sale.action_client_pos_menu') pos_sessions.login() diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 8dbcb4f1793..d1deb839071 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -31,7 +31,7 @@ class PosConfig(models.Model): def _default_sale_journal(self): journal = self.env.ref('point_of_sale.pos_sale_journal', raise_if_not_found=False) - if journal and journal.company_id == self.env.user.company_id: + if journal and journal.sudo().company_id == self.env.user.company_id: return journal return self._default_invoice_journal() @@ -137,9 +137,12 @@ class PosConfig(models.Model): @api.depends('session_ids') def _compute_current_session(self): for pos_config in self: - session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and not r.state == 'closed') - pos_config.current_session_id = session - pos_config.current_session_state = session.state + session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \ + not r.state == 'closed' and \ + '(RESCUE FOR' not in r.name) + # sessions ordered by id desc + pos_config.current_session_id = session and session[0].id or False + pos_config.current_session_state = session and session[0].state or False @api.depends('session_ids') def _compute_last_session(self): @@ -159,7 +162,8 @@ class PosConfig(models.Model): @api.depends('session_ids') def _compute_current_session_user(self): for pos_config in self: - pos_config.pos_session_username = pos_config.session_ids.filtered(lambda s: s.state == 'opened').user_id.name + session = pos_config.session_ids.filtered(lambda s: s.state == 'opened' and '(RESCUE FOR' not in s.name) + pos_config.pos_session_username = session and session[0].user_id.name or False @api.constrains('company_id', 'stock_location_id') def _check_company_location(self): @@ -207,7 +211,7 @@ class PosConfig(models.Model): @api.model def create(self, values): - IrSequence = self.env['ir.sequence'] + IrSequence = self.env['ir.sequence'].sudo() val = { 'name': _('POS Order %s') % values['name'], 'padding': 4, diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index c4db68c0311..1e3a56fb00d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -66,11 +66,18 @@ class PosOrder(models.Model): closed_session.id, order['name'], order['amount_total']) + rescue_session = PosSession.search([ + ('name', 'like', '(RESCUE FOR %(session)s)' % {'session': closed_session.name}), + ('state', 'not in', ('closed', 'closing_control')), + ], limit=1) + if rescue_session: + _logger.warning('reusing recovery session %s for saving order %s', rescue_session.name, order['name']) + return rescue_session + _logger.warning('attempting to create recovery session for saving order %s', order['name']) new_session = PosSession.create({ 'config_id': closed_session.config_id.id, - 'name': _('(RESCUE FOR %(session)s)') % {'session': closed_session.name}, - 'rescue': True, # avoid conflict with live sessions + 'name': '(RESCUE FOR %(session)s)' % {'session': closed_session.name}, }) # bypass opening_control (necessary when using cash control) new_session.action_pos_session_open() @@ -334,7 +341,12 @@ class PosOrder(models.Model): name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/') company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now) - user_id = fields.Many2one('res.users', string='Salesman', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", default=lambda self: self.env.uid) + user_id = fields.Many2one( + comodel_name='res.users', string='Salesman', + help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", + default=lambda self: self.env.uid, + states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]}, + ) amount_tax = fields.Float(compute='_compute_amount_all', string='Taxes', digits=0) amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) amount_paid = fields.Float(compute='_compute_amount_all', string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits=0) @@ -359,12 +371,22 @@ class PosOrder(models.Model): account_move = fields.Many2one('account.move', string='Journal Entry', readonly=True, copy=False) picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False) picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Picking Type") - location_id = fields.Many2one('stock.location', related='session_id.config_id.stock_location_id', string="Location", store=True) + location_id = fields.Many2one( + comodel_name='stock.location', + related='session_id.config_id.stock_location_id', + string="Location", store=True, + readonly=True, + ) note = fields.Text(string='Internal Notes') nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0) pos_reference = fields.Char(string='Receipt Ref', readonly=True, copy=False) sale_journal = fields.Many2one('account.journal', related='session_id.config_id.journal_id', string='Sales Journal', store=True, readonly=True) - fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', default=lambda self: self._default_session().config_id.default_fiscal_position_id) + fiscal_position_id = fields.Many2one( + comodel_name='account.fiscal.position', string='Fiscal Position', + default=lambda self: self._default_session().config_id.default_fiscal_position_id, + readonly=True, + states={'draft': [('readonly', False)]}, + ) @api.depends('statement_ids', 'lines.price_subtotal_incl', 'lines.discount') def _compute_amount_all(self): @@ -540,6 +562,8 @@ class PosOrder(models.Model): Move = self.env['stock.move'] StockWarehouse = self.env['stock.warehouse'] for order in self: + if not order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']): + continue address = order.partner_id.address_get(['delivery']) or {} picking_type = order.picking_type_id return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id @@ -569,7 +593,7 @@ class PosOrder(models.Model): 'location_id': location_id, 'location_dest_id': destination_id, } - pos_qty = any([x.qty >= 0 for x in order.lines]) + pos_qty = any([x.qty > 0 for x in order.lines]) if pos_qty: order_picking = Picking.create(picking_vals.copy()) order_picking.message_post(body=message) @@ -607,8 +631,8 @@ class PosOrder(models.Model): # when the pos.config has no picking_type_id set only the moves will be created if moves and not return_picking and not order_picking: - moves.action_confirm() - moves.force_assign() + moves.action_assign() + moves.filtered(lambda m: m.state in ['confirmed', 'waiting']).force_assign() moves.filtered(lambda m: m.product_id.tracking == 'none').action_done() return True @@ -616,7 +640,7 @@ class PosOrder(models.Model): def _force_picking_done(self, picking): """Force picking in order to be set as done.""" self.ensure_one() - picking.action_confirm() + picking.action_assign() picking.force_assign() self.set_pack_operation_lot(picking) if not any([(x.product_id.tracking != 'none') for x in picking.pack_operation_ids]): diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 4335f1d3f2b..a664e639c3b 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -128,12 +128,20 @@ class PosSession(models.Model): @api.constrains('user_id', 'state') def _check_unicity(self): # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user - if self.search_count([('state', 'not in', ('closed', 'closing_control')), ('user_id', '=', self.user_id.id)]) > 1: + if self.search_count([ + ('state', 'not in', ('closed', 'closing_control')), + ('user_id', '=', self.user_id.id), + ('name', 'not like', 'RESCUE FOR'), + ]) > 1: raise ValidationError(_("You cannot create two active sessions with the same responsible!")) @api.constrains('config_id') def _check_pos_config(self): - if self.search_count([('state', '!=', 'closed'), ('config_id', '=', self.config_id.id)]) > 1: + if self.search_count([ + ('state', '!=', 'closed'), + ('config_id', '=', self.config_id.id), + ('name', 'not like', 'RESCUE FOR'), + ]) > 1: raise ValidationError(_("You cannot create two active sessions related to the same point of sale!")) @api.model diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 02d1123bcc3..b9370802dd6 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -11,6 +11,7 @@ var CrashManager = require('web.CrashManager'); var _t = core._t; +var _lt = core._lt; var QWeb = core.qweb; /* -------- The Order Selector -------- */ @@ -704,7 +705,7 @@ var Chrome = PosBaseWidget.extend({ 'widget': HeaderButtonWidget, 'append': '.pos-rightheader', 'args': { - label: _t('Close'), + label: _lt('Close'), action: function(){ var self = this; if (!this.confirmed) { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 80ea86b9be9..4cb1cc08296 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1318,7 +1318,9 @@ exports.Orderline = Backbone.Model.extend({ this.trigger('change',this); }, get_unit_price: function(){ - return round_di(this.price || 0, this.pos.dp['Product Price']) + var digits = this.pos.dp['Product Price']; + // round and truncate to mimic _sybmbol_set behavior + return parseFloat(round_di(this.price || 0, digits).toFixed(digits)); }, get_unit_display_price: function(){ if (this.pos.config.iface_tax_included) { diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 5b8933c4cc7..8c4efac4d0e 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1355,6 +1355,16 @@ var ClientListScreenWidget = ScreenWidget.extend({ contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner}))); this.toggle_save_button(); + // Browsers attempt to scroll invisible input elements + // into view (eg. when hidden behind keyboard). They don't + // seem to take into account that some elements are not + // scrollable. + contents.find('input').blur(function() { + setTimeout(function() { + self.$('.window').scrollTop(0); + }, 0); + }); + contents.find('.image-uploader').on('change',function(event){ self.load_image_file(event.target.files[0],function(res){ if (res) { @@ -1404,7 +1414,9 @@ var ReceiptScreenWidget = ScreenWidget.extend({ this.render_change(); this.render_receipt(); - + this.handle_auto_print(); + }, + handle_auto_print: function() { if (this.should_auto_print()) { this.print(); if (this.should_close_immediately()){ @@ -1413,7 +1425,6 @@ var ReceiptScreenWidget = ScreenWidget.extend({ } else { this.lock_screen(false); } - }, should_auto_print: function() { return this.pos.config.iface_print_auto && !this.pos.get_order()._printed; diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index ebf971d2ec3..4c5597979c6 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -10,12 +10,16 @@ class TestPointOfSaleCommon(common.TransactionCase): self.AccountBankStatementLine = self.env['account.bank.statement.line'] self.PosMakePayment = self.env['pos.make.payment'] self.PosOrder = self.env['pos.order'] + self.PosSession = self.env['pos.session'] self.company_id = self.ref('base.main_company') self.product3 = self.env.ref('product.product_product_3') self.product4 = self.env.ref('product.product_product_4') self.partner1 = self.env.ref('base.res_partner_1') self.partner4 = self.env.ref('base.res_partner_4') self.pos_config = self.env.ref('point_of_sale.pos_config_main') + self.carotte = self.env.ref('point_of_sale.carotte') + self.courgette = self.env.ref('point_of_sale.courgette') + self.onions = self.env.ref('point_of_sale.Onions') # create a new session self.pos_order_session0 = self.env['pos.session'].create({ diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index 51e0bf5a69b..a22f5addc55 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -4,8 +4,8 @@ import time import os import odoo -from odoo import tools, report as odoo_report -from odoo.tools import float_compare, test_reports +from odoo import fields, tools, report as odoo_report +from odoo.tools import float_compare, mute_logger, test_reports from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon @@ -411,3 +411,138 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I confirm the bank statement using Confirm button self.AccountBankStatement.button_confirm_bank() + + def test_create_from_ui(self): + """ + Simulation of sales coming from the interface, even after closing the session + """ + # I click on create a new session button + self.pos_config.open_session_cb() + + current_session = self.pos_config.current_session_id + num_starting_orders = len(current_session.order_ids) + + carrot_order = {'data': + {'amount_paid': 0.9, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 0.9, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 42, + 'pack_lot_ids': [], + 'price_unit': 0.9, + 'product_id': self.carotte.id, + 'qty': 1, + 'tax_ids': []}]], + 'name': 'Order 00042-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': 2, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 0.9, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00042-003-0014', + 'user_id': self.env.uid}, + 'id': '00042-003-0014', + 'to_invoice': False} + + zucchini_order = {'data': + {'amount_paid': 1.2, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 1.1, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'pack_lot_ids': [], + 'price_unit': 1.2, + 'product_id': self.courgette.id, + 'qty': 1, + 'tax_ids': []}]], + 'name': 'Order 00043-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 1.2, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00043-003-0014', + 'user_id': self.env.uid}, + 'id': '00043-003-0014', + 'to_invoice': False} + + onions_order = {'data': + {'amount_paid': 1.28, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 1.28, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'pack_lot_ids': [], + 'price_unit': 1.28, + 'product_id': self.onions.id, + 'qty': 1, + 'tax_ids': [[6, False, self.onions.taxes_id.ids]]}]], + 'name': 'Order 00044-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 1.28, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00044-003-0014', + 'user_id': self.env.uid}, + 'id': '00044-003-0014', + 'to_invoice': False} + + + # I create an order on an open session + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded") + + # I resubmit the same order + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Resubmitted order was not skipped") + + # I close the session + current_session.action_pos_session_closing_control() + self.assertEqual(current_session.state, 'closed', "Session was not properly closed") + self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed") + + # I keep selling after the session is closed + with mute_logger('odoo.addons.point_of_sale.models.pos_order'): + self.PosOrder.create_from_ui([zucchini_order, onions_order]) + rescue_session = self.PosSession.search([ + ('config_id', '=', self.pos_config.id), + ('state', '=', 'opened'), + ]) + self.assertEqual(len(rescue_session), 1, "One (and only one) rescue session should be created for orphan orders") + self.assertIn("(RESCUE FOR %s)" % current_session.name, rescue_session.name, "Rescue session is not linked to the previous one") + self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders") + + # I close the rescue session + rescue_session.action_pos_session_closing_control() + self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed") diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml index 1a377c34db8..052a9c9a822 100644 --- a/addons/point_of_sale/views/pos_session_view.xml +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -157,7 +157,7 @@ - + diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 006a24889d7..afe89d3cebd 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -550,13 +550,34 @@ class PurchaseOrderLine(models.Model): line = super(PurchaseOrderLine, self).create(values) if line.order_id.state == 'purchase': line.order_id._create_picking() + msg = _("Extra line with %s ") % (line.product_id.display_name,) + line.order_id.message_post(body=msg) return line @api.multi def write(self, values): + orders = False + if 'product_qty' in values: + changed_lines = self.filtered(lambda x: x.order_id.state == 'purchase') + if changed_lines: + orders = changed_lines.mapped('order_id') + for order in orders: + order_lines = changed_lines.filtered(lambda x: x.order_id == order) + msg = "" + if any([values['product_qty'] < x.product_qty for x in order_lines]): + msg += "" + _('The ordered quantity has been decreased. Do not forget to take it into account on your bills and receipts.') + '
' + msg += "
    " + for line in order_lines: + msg += "
  • %s:" % (line.product_id.display_name,) + msg += "
    " + _("Ordered Quantity") + ": %s -> %s
    " % (line.product_qty, float(values['product_qty']),) + if line.product_id.type in ('product', 'consu'): + msg += _("Received Quantity") + ": %s
    " % (line.qty_received,) + msg += _("Billed Quantity") + ": %s
  • " % (line.qty_invoiced,) + msg += "
" + order.message_post(body=msg) result = super(PurchaseOrderLine, self).write(values) - orders = self.filtered(lambda x: x.order_id.state == 'purchase').mapped('order_id') - orders._create_picking() + if orders: + orders._create_picking() return result name = fields.Text(string='Description', required=True) @@ -950,6 +971,12 @@ class ProcurementOrder(models.Model): 'group_id': group } + def _make_po_select_supplier(self, suppliers): + """ Method intended to be overridden by customized modules to implement any logic in the + selection of supplier. + """ + return suppliers[0] + @api.multi def make_po(self): cache = {} @@ -959,7 +986,7 @@ class ProcurementOrder(models.Model): if not suppliers: procurement.message_post(body=_('No vendor associated to product %s. Please set one to fix this procurement.') % (procurement.product_id.name)) continue - supplier = suppliers[0] + supplier = procurement._make_po_select_supplier(suppliers) partner = supplier.name gpo = procurement.rule_id.group_propagation_option diff --git a/addons/report/models/report.py b/addons/report/models/report.py index e057578a0cc..28e6467aee2 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -461,8 +461,11 @@ class Report(models.Model): out, err = process.communicate() if process.returncode not in [0, 1]: - raise UserError(_('Wkhtmltopdf failed (error code: %s). ' - 'Message: %s') % (str(process.returncode), err)) + if process.returncode == -11: + message = _('Wkhtmltopdf failed (error code: %s). Memory limit too low or maximum file number of subprocess reached. Message : %s') + else: + message = _('Wkhtmltopdf failed (error code: %s). Message: %s') + raise UserError(message % (str(process.returncode), err[-1000:])) # Save the pdf in attachment if marked if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]): diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index a389b5f9691..713308aa7a1 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -84,6 +84,10 @@ class AccountInvoice(models.Model): return report_pages + @api.multi + def get_delivery_partner_id(self): + self.ensure_one() + return self.partner_shipping_id.id or super(AccountInvoice, self).get_delivery_partner_id() class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 2e0d017c8c2..d8d03f5ef1e 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -61,7 +61,9 @@ class SaleOrder(models.Model): for inv in invoice_ids: refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)]) - line_invoice_status = [line.invoice_status for line in order.order_line] + # Ignore the status of the deposit product + deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() + line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id] if order.state not in ('sale', 'done'): invoice_status = 'no' @@ -507,11 +509,11 @@ class SaleOrder(models.Model): group = tax.tax_group_id res.setdefault(group, 0.0) amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty, - product=line.product_id, partner=line.order_partner_id)['taxes'][0]['amount'] + product=line.product_id, partner=self.partner_shipping_id)['taxes'][0]['amount'] res[group] += amount if tax.include_base_amount: base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1, product=line.product_id, - partner=line.order_partner_id)['taxes'][0]['amount'] + partner=self.partner_shipping_id)['taxes'][0]['amount'] res = sorted(res.items(), key=lambda l: l[0].sequence) res = map(lambda l: (l[0].name, l[1]), res) return res @@ -558,7 +560,7 @@ class SaleOrderLine(models.Model): """ for line in self: price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id) + taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id) line.update({ 'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])), 'price_total': taxes['total_included'], @@ -624,7 +626,7 @@ class SaleOrderLine(models.Model): fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id # If company_id is set, always filter taxes by the company taxes = line.product_id.taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) - line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_id) if fpos else taxes + line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes @api.multi def _prepare_order_line_procurement(self, group_id=False): @@ -688,16 +690,37 @@ class SaleOrderLine(models.Model): line = super(SaleOrderLine, self).create(values) if line.state == 'sale': line._action_procurement_create() + msg = _("Extra line with %s ") % (line.product_id.display_name,) + line.order_id.message_post(body=msg) return line @api.multi def write(self, values): lines = False + changed_lines = False if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') lines = self.filtered( lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) + changed_lines = self.filtered( + lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0) + if changed_lines: + orders = self.mapped('order_id') + for order in orders: + order_lines = changed_lines.filtered(lambda x: x.order_id == order) + msg = "" + if any([values['product_uom_qty'] < x.product_uom_qty for x in order_lines]): + msg += "" + _('The ordered quantity has been decreased. Do not forget to take it into account on your invoices and delivery orders.') + '' + msg += "
    " + for line in order_lines: + msg += "
  • %s:" % (line.product_id.display_name,) + msg += "
    " + _("Ordered Quantity") + ": %s -> %s
    " % (line.product_uom_qty, float(values['product_uom_qty']),) + if line.product_id.type in ('consu', 'product'): + msg += _("Delivered Quantity") + ": %s
    " % (line.qty_delivered,) + msg += _("Invoiced Quantity") + ": %s
    " % (line.qty_invoiced,) + msg += "
" + order.message_post(body=msg) result = super(SaleOrderLine, self).write(values) if lines: lines._action_procurement_create() @@ -821,10 +844,16 @@ class SaleOrderLine(models.Model): @api.multi def _get_display_price(self, product): # TO DO: move me in master/saas-16 on sale.order - if self.order_id.pricelist_id.discount_policy == 'without_discount': + if self.order_id.pricelist_id.discount_policy == 'with_discount': + return product.with_context(pricelist=self.order_id.pricelist_id.id).price + price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + pricelist_item = self.env['product.pricelist.item'].browse(rule_id) + if (pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id.discount_policy == 'with_discount'): + price, rule_id = pricelist_item.base_pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + return price + else: from_currency = self.order_id.company_id.currency_id return from_currency.compute(product.lst_price, self.order_id.pricelist_id.currency_id) - return product.with_context(pricelist=self.order_id.pricelist_id.id).price @api.multi @api.onchange('product_id') @@ -964,7 +993,7 @@ class SaleOrderLine(models.Model): new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) new_list_price = self.env['account.tax']._fix_tax_included_price(new_list_price, self.product_id.taxes_id, self.tax_id) - if price != 0 and new_list_price != 0: + if new_list_price != 0: if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id: # new_list_price is in company's currency while price in pricelist currency new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id) diff --git a/addons/sale/models/sale_analytic.py b/addons/sale/models/sale_analytic.py index acf3fd48682..e937292d818 100644 --- a/addons/sale/models/sale_analytic.py +++ b/addons/sale/models/sale_analytic.py @@ -12,7 +12,19 @@ class SaleOrderLine(models.Model): lines = {} if not domain: # To filter on analyic lines linked to an expense - domain = [('so_line', 'in', self.ids), ('amount', '<=', 0.0)] + expense_type_id = self.env.ref('account.data_account_type_expenses', raise_if_not_found=False) + expense_type_id = expense_type_id and expense_type_id.id + domain = [ + ('so_line', 'in', self.ids), + '|', + ('amount', '<', 0), + '&', + ('amount', '=', 0), + '|', + ('move_id', '=', False), + ('move_id.account_id.user_type_id', '=', expense_type_id) + ] + data = self.env['account.analytic.line'].read_group( domain, ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 4e5eee48591..0254c3dbe0e 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -142,5 +142,18 @@ class SaleOrderLine(models.Model): def _compute_analytic(self, domain=None): if not domain: # To filter on analyic lines linked to an expense - domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('project_id', '!=', False)] + expense_type_id = self.env.ref('account.data_account_type_expenses', raise_if_not_found=False) + expense_type_id = expense_type_id and expense_type_id.id + domain = [ + ('so_line', 'in', self.ids), + '|', + '|', + ('amount', '<', 0.0), + ('project_id', '!=', False), + '&', + ('amount', '=', 0), + '|', + ('move_id', '=', False), + ('move_id.account_id.user_type_id', '=', expense_type_id) + ] return super(SaleOrderLine, self)._compute_analytic(domain=domain) diff --git a/addons/sales_team/data/sales_team_data.xml b/addons/sales_team/data/sales_team_data.xml index 4f2397c5731..2af8a2f7992 100644 --- a/addons/sales_team/data/sales_team_data.xml +++ b/addons/sales_team/data/sales_team_data.xml @@ -4,10 +4,12 @@ Sales + Website + diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py index a05520d422b..c4cb8b74301 100644 --- a/addons/sales_team/models/crm_team.py +++ b/addons/sales_team/models/crm_team.py @@ -24,9 +24,11 @@ class CrmTeam(models.Model): def _get_default_team_id(self, user_id=None): if not user_id: user_id = self.env.uid - team_id = self.env['crm.team'].sudo().search( - ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], - limit=1) + company_id = self.sudo(user_id).company_id.id + team_id = self.env['crm.team'].sudo().search([ + '|', ('user_id', '=', user_id), ('member_ids', '=', user_id), + '|', ('company_id', '=', False), ('company_id', 'child_of', [company_id]) + ], limit=1) if not team_id and 'default_team_id' in self.env.context: team_id = self.env['crm.team'].browse(self.env.context.get('default_team_id')) if not team_id: diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index 87f2af00d8e..b49bd54c48b 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -277,23 +277,27 @@ class ProcurementOrder(models.Model): @api.model def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False): """ Create procurements based on orderpoints. - :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. + :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing + 1000 orderpoints. This is appropriate for batch jobs only. """ - if use_new_cursor: - cr = registry(self._cr.dbname).cursor() - self = self.with_env(self.env(cr=cr)) OrderPoint = self.env['stock.warehouse.orderpoint'] - Procurement = self.env['procurement.order'] - ProcurementAutorundefer = Procurement.with_context(procurement_autorun_defer=True) - procurement_list = [] - orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search( - company_id and [('company_id', '=', company_id)] or [], - order=self._procurement_from_orderpoint_get_order()) + domain = [('company_id', '=', company_id)] if company_id else [] + domain += [('product_id.active', '=', True)] + orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain, + order=self._procurement_from_orderpoint_get_order()).ids while orderpoints_noprefetch: - orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000].ids) + if use_new_cursor: + cr = registry(self._cr.dbname).cursor() + self = self.with_env(self.env(cr=cr)) + OrderPoint = self.env['stock.warehouse.orderpoint'] + Procurement = self.env['procurement.order'] + ProcurementAutorundefer = Procurement.with_context(procurement_autorun_defer=True) + procurement_list = [] + + orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000]) orderpoints_noprefetch = orderpoints_noprefetch[1000:] # Calculate groups that can be executed together @@ -343,7 +347,7 @@ class ProcurementOrder(models.Model): except OperationalError: if use_new_cursor: - orderpoints_noprefetch += orderpoint.id + orderpoints_noprefetch += [orderpoint.id] cr.rollback() continue else: @@ -367,8 +371,6 @@ class ProcurementOrder(models.Model): if use_new_cursor: cr.commit() + cr.close() - if use_new_cursor: - cr.commit() - cr.close() return {} diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 0978adaec8a..c9a50f3f704 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -349,7 +349,7 @@ class StockMove(models.Model): raise UserError(_('Cannot unreserve a done move')) self.quants_unreserve() if not self.env.context.get('no_state_change'): - waiting = self.filtered(lambda move: move.get_ancestors()) + waiting = self.filtered(lambda move: move.procure_method == 'make_to_order' or move.get_ancestors()) waiting.write({'state': 'waiting'}) (self - waiting).write({'state': 'confirmed'}) @@ -411,15 +411,8 @@ class StockMove(models.Model): type (moves should already have them identical). Otherwise, create a new picking to assign them to. """ Picking = self.env['stock.picking'] - - # If this method is called in batch by a write on a one2many and - # at some point had to create a picking, some next iterations could - # try to find back the created picking. As we look for it by searching - # on some computed fields, we have to force a recompute, else the - # record won't be found. - self.recompute() - for move in self: + recompute = False picking = Picking.search([ ('group_id', '=', move.group_id.id), ('location_id', '=', move.location_id.id), @@ -428,8 +421,17 @@ class StockMove(models.Model): ('printed', '=', False), ('state', 'in', ['draft', 'confirmed', 'waiting', 'partially_available', 'assigned'])], limit=1) if not picking: + recompute = True picking = Picking.create(move._get_new_picking_values()) move.write({'picking_id': picking.id}) + + # If this method is called in batch by a write on a one2many and + # at some point had to create a picking, some next iterations could + # try to find back the created picking. As we look for it by searching + # on some computed fields, we have to force a recompute, else the + # record won't be found. + if recompute: + move.recompute() return True _picking_assign = assign_picking @@ -690,7 +692,7 @@ class StockMove(models.Model): if len(reserved_quant_ids) == 0 and move.partially_available: vals['partially_available'] = False if move.state == 'assigned': - if move.find_move_ancestors(): + if move.procure_method == 'make_to_order' or move.find_move_ancestors(): vals['state'] = 'waiting' else: vals['state'] = 'confirmed' diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index c36ba994f7d..014869345c2 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -19,6 +19,9 @@
+
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> +

diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 3ee20467a88..32f77c06267 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1726,4 +1726,69 @@ class TestStockFlow(TestStockCommon): # force assign on the delivery order, it should be assigned picking_out.force_assign() - self.assertEquals(picking_out.state, "assigned") \ No newline at end of file + self.assertEquals(picking_out.state, "assigned") + + def test_74_move_state_waiting_mto(self): + """ This test will check that when a move is unreserved, it state change to 'waiting' if + it has ancestors or is has a 'procure_method' equal to 'make_to_order' else the state + changes to 'confirmed'. + """ + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + move_mto_alone = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'procure_method':'make_to_order'}) + move_with_ancestors = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + the_ancestor = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'move_dest_id': move_with_ancestors.id}) + other_move = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + move_mto_alone.action_confirm() + move_with_ancestors.action_confirm() + other_move.action_confirm() + + move_mto_alone.do_unreserve() + move_with_ancestors.do_unreserve() + other_move.do_unreserve() + + self.assertEquals(move_mto_alone.state, "waiting") + self.assertEquals(move_with_ancestors.state, "waiting") + self.assertEquals(other_move.state, "confirmed") + + move_mto_alone.recalculate_move_state() + move_with_ancestors.recalculate_move_state() + other_move.recalculate_move_state() + + self.assertEquals(move_mto_alone.state, "waiting") + self.assertEquals(move_with_ancestors.state, "waiting") + self.assertEquals(other_move.state, "confirmed") diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml index 62d1e3d748c..7a8caea9e5a 100644 --- a/addons/stock/views/stock_warehouse_views.xml +++ b/addons/stock/views/stock_warehouse_views.xml @@ -98,6 +98,7 @@ stock.warehouse + diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 4706bbc6b45..9fec58c20ab 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -188,7 +188,7 @@ class StockMove(models.Model): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} - for move in self.filtered(lambda move: move.location_id.usage == 'supplier' and move.product_id.cost_method == 'average'): + for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move.product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id] # if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use. diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index 2427a8ca0ca..12134cc0f0f 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -129,6 +129,7 @@ class LandedCost(models.Model): if quant.location_id.usage != 'internal': qty_out += quant.qty line._create_accounting_entries(move, qty_out) + move.assert_balanced() cost.write({'state': 'done', 'account_move_id': move.id}) move.post() return True @@ -326,7 +327,7 @@ class AdjustmentLines(models.Model): Generate the account.move.line values to track the landed cost. Afterwards, for the goods that are already out of stock, we should create the out moves """ - AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False, recompute=False) base_line = { 'name': self.name, @@ -389,5 +390,4 @@ class AdjustmentLines(models.Model): AccountMoveLine.create(debit_line) AccountMoveLine.create(credit_line) - move.assert_balanced() return True diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index cf6c8be5302..ed3e34762e5 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -651,6 +651,8 @@ class Database(http.Controller): @http.route('/web/database/create', type='http', auth="none", methods=['POST'], csrf=False) def create(self, master_pwd, name, lang, password, **post): try: + if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', name): + raise Exception(_('Invalid database name. Only alphanumerical characters and underscore are allowed.')) # country code could be = "False" which is actually True in python country_code = post.get('country_code') or False dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code]) @@ -663,6 +665,8 @@ class Database(http.Controller): @http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False) def duplicate(self, master_pwd, name, new_name): try: + if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', new_name): + raise Exception(_('Invalid database name. Only alphanumerical characters and underscore are allowed.')) dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name]) return http.local_redirect('/web/database/manager') except Exception, e: diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index d3135f86ba6..c74b89af11b 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -207,9 +207,15 @@ var DataExport = Dialog.extend({ this.records = {}; this.dataset = dataset; this.exports = new data.DataSetSearch(this, 'ir.exports', this.dataset.get_context()); - + this.row_index = 0; this.row_index_level = 0; + + // The default for the ".modal_content" element is "max-height: 100%;" + // but we want it to always expand to "height: 100%;" for this modal. + // This can be achieved thanks to LESS modification without touching + // the ".modal-content" rules... but not with Internet explorer (11). + this.$modal.find(".modal-content").css("height", "100%"); }, start: function() { var self = this; @@ -261,7 +267,7 @@ var DataExport = Dialog.extend({ function do_setup_export_formats(formats) { var $fmts = self.$('.o_export_format'); - + _.each(formats, function(format, i) { var $radio = $('', {type: 'radio', value: format.tag, name: 'o_export_format_name'}); var $label = $('

TaxBase Amount
+ + Debit Credit BalanceCurrencyCurrency
- + + + +
- -u web --stop-after-init + $ python /usr/bin/odoo.py -d -i web_enterprise --stop-after-init * You should be able to connect to your Odoo Enterprise instance using your usual mean of identification. You can then link your database with your Odoo Enterprise Subscription by entering the code you received @@ -61,11 +61,11 @@ have your own favourite. You may need to adapt sections to your usual workflow. * Shutdown your server * Backup your community database * Update the ``--addons-path`` parameter of your launch command (see :ref:`setup/install/source`) -* Update the web module by using +* Install the web_enterprise module by using .. code-block:: console - $ -d -u web --stop-after-init + $ -d -i web_enterprise --stop-after-init Depending on the size of your database, this may take some time. @@ -97,7 +97,7 @@ On Windows .. code-block:: console - $ odoo.exe -d -u web --stop-after-init + $ odoo.exe -d -i web_enterprise --stop-after-init * No need to manually launch the server, the service is running. You should be able to connect to your Odoo Enterprise instance using your usual diff --git a/odoo/addons/base/ir/ir_filters.xml b/odoo/addons/base/ir/ir_filters.xml index d21c6ad25d5..f46ec82c7f6 100644 --- a/odoo/addons/base/ir/ir_filters.xml +++ b/odoo/addons/base/ir/ir_filters.xml @@ -24,7 +24,7 @@ ir.filters.own.rule.delete - [('create_uid', '=', user.id)] + [('user_id', '=', user.id)] diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 98410ba110b..f67d9c1dc5f 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -9,6 +9,7 @@ from collections import defaultdict from odoo import api, fields, models, SUPERUSER_ID, tools, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.modules.registry import Registry +from odoo.osv import expression from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -292,6 +293,25 @@ class IrModelFields(models.Model): raise UserError(_("The Selection Options expression is not a valid Pythonic expression." "Please provide an expression in the [('key','Label'), ...] format.")) + @api.constrains('name', 'state') + def _check_name(self): + for field in self: + if field.state == 'manual' and not field.name.startswith('x_'): + raise ValidationError(_("Custom fields must have a name that starts with 'x_' !")) + try: + models.check_pg_name(field.name) + except ValidationError: + msg = _("Field names can only contain characters, digits and underscores (up to 63).") + raise ValidationError(msg) + + @api.constrains('model', 'name') + def _unique_name(self): + # fix on stable branch (to be converted into an SQL constraint) + for field in self: + count = self.search_count([('model', '=', field.model), ('name', '=', field.name)]) + if count > 1: + raise ValidationError(_("Field names must be unique per model.")) + _sql_constraints = [ ('size_gt_zero', 'CHECK (size>=0)', 'Size of the field cannot be negative.'), ] @@ -449,6 +469,10 @@ class IrModelFields(models.Model): This method prevents the modification/deletion of many2one fields that have an inverse one2many, for instance. """ + self = self.filtered(lambda record: record.state == 'manual') + if not self: + return + for record in self: model = self.env[record.model] field = model._fields[record.name] @@ -462,6 +486,23 @@ class IrModelFields(models.Model): msg = _("The field '%s' cannot be removed because the field '%s' depends on it.") raise UserError(msg % (field, model._field_inverses[field][0])) + # remove fields from registry, and check that views are not broken + fields = [self.env[record.model]._pop_field(record.name) for record in self] + domain = expression.OR([('arch_db', 'like', record.name)] for record in self) + views = self.env['ir.ui.view'].search(domain) + try: + for view in views: + view._check_xml() + except Exception: + raise UserError("\n".join([ + _("Cannot rename/delete fields that are still present in views:"), + _("Fields:") + " " + ", ".join(map(str, fields)), + _("View:") + " " + view.name, + ])) + finally: + # the registry has been modified, restore it + self.pool.setup_models(self._cr) + @api.multi def unlink(self): if not self: @@ -504,9 +545,6 @@ class IrModelFields(models.Model): res = super(IrModelFields, self).create(vals) if vals.get('state', 'manual') == 'manual': - if not vals['name'].startswith('x_'): - raise UserError(_("Custom fields must have a name that starts with 'x_' !")) - if vals.get('relation') and not self.env['ir.model'].search([('model', '=', vals['relation'])]): raise UserError(_("Model %s does not exist!") % vals['relation']) @@ -569,12 +607,6 @@ class IrModelFields(models.Model): item._prepare_update() if column_rename: raise UserError(_('Can only rename one field at a time!')) - if vals['name'] in obj._fields: - raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name']) - if vals.get('state', 'manual') == 'manual' and not vals['name'].startswith('x_'): - raise UserError(_('New field name must still start with x_ , because it is a custom field!')) - if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']: - raise ValueError('Invalid character in column name') column_rename = (obj._table, item.name, vals['name'], item.index) # We don't check the 'state', because it might come from the context diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py index f0c7d576672..4beec41db7c 100644 --- a/odoo/addons/base/ir/ir_qweb/fields.py +++ b/odoo/addons/base/ir/ir_qweb/fields.py @@ -340,7 +340,7 @@ class MonetaryConverter(models.AbstractModel): if not options.get('display_currency') and field.type == 'monetary' and field.currency_field: options['display_currency'] = record[field.currency_field] - return self.value_to_html(record[field_name], options) + return super(MonetaryConverter, self).record_to_html(record, field_name, options) TIMEDELTA_UNITS = ( ('year', 3600 * 24 * 365), diff --git a/odoo/addons/base/res/res_company_view.xml b/odoo/addons/base/res/res_company_view.xml index 95bafe4f334..4746deeb1b1 100644 --- a/odoo/addons/base/res/res_company_view.xml +++ b/odoo/addons/base/res/res_company_view.xml @@ -1,7 +1,7 @@ - + res.company.form diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index e92aaf5315e..da025eeca78 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -744,7 +744,7 @@ class GroupsView(models.Model): xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace") xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS")) xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8") - view.with_context(lang=None).write({'arch': xml_content}) + view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False}) def get_application_groups(self, domain): """ Return the non-share groups that satisfy ``domain``. """ diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 932268d717c..104c4a6dfe1 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -683,8 +683,8 @@ class TestAutoJoin(TransactionCase): self.assertEqual(partners, p_aa, "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 3, - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") + self.assertEqual(len(self.query_list), 2, + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 2 queries (1 in res_partner_bank, 1 on res_partner)") sql_query = self.query_list[0].get_sql() self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect main table") @@ -692,16 +692,16 @@ class TestAutoJoin(TransactionCase): expected = "%s::text like %s" % (unaccent('"res_partner_bank"."sanitized_acc_number"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect where condition") - + self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect parameter") - sql_query = self.query_list[2].get_sql() + sql_query = self.query_list[1].get_sql() self.assertIn('res_partner', sql_query[0], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect main table") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect main table") self.assertIn('"res_partner"."id" in (%s)', sql_query[1], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect where condition") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect where condition") self.assertIn(p_aa.id, sql_query[2], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect parameter") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect parameter") # Do: cascaded one2many without _auto_join self._reinit_mock() @@ -710,8 +710,8 @@ class TestAutoJoin(TransactionCase): self.assertEqual(partners, p_a + p_b, "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 5, - "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 4 on res_partner)") + self.assertEqual(len(self.query_list), 3, + "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") # Do: one2many with _auto_join patch_auto_join(partner_obj, 'bank_ids', True) @@ -943,7 +943,7 @@ class TestAutoJoin(TransactionCase): self.assertLessEqual(p_a + p_b, partners, "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 5, + self.assertEqual(len(self.query_list), 4, "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") # Do: ('child_ids.state_id.country_id.code', 'like', '..') with _auto_join diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 74d220b05bb..62711a8eab5 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.tools import mute_logger import odoo.tests.common as common @@ -181,3 +181,112 @@ class TestServerActions(TestServerActionsBase): self.action.write({ 'child_ids': [(6, 0, [self.action.id])] }) + + +class TestCustomFields(common.TransactionCase): + MODEL = 'res.partner' + + def setUp(self): + # use a test cursor instead of a real cursor + registry = odoo.registry() + registry.enter_test_mode() + fnames = set(registry[self.MODEL]._fields) + + @self.addCleanup + def callback(): + registry.leave_test_mode() + # the tests may have modified the registry, reset it + with registry.cursor() as cr: + registry.clear_manual_fields() + registry.setup_models(cr) + assert set(registry[self.MODEL]._fields) == fnames + + super(TestCustomFields, self).setUp() + + # do not reload the registry after removing a field + self.env = self.env(context={'_force_unlink': True}) + + def create_field(self, name): + """ create a custom field and return it """ + model = self.env['ir.model'].search([('model', '=', self.MODEL)]) + field = self.env['ir.model.fields'].create({ + 'model_id': model.id, + 'name': name, + 'field_description': name, + 'ttype': 'char', + }) + self.assertIn(name, self.env[self.MODEL]._fields) + return field + + def create_view(self, name): + """ create a view with the given field name """ + return self.env['ir.ui.view'].create({ + 'name': 'yet another view', + 'model': self.MODEL, + 'arch': '' % name, + }) + + def test_create_custom(self): + """ custom field names must be start with 'x_' """ + with self.assertRaises(ValidationError): + self.create_field('foo') + + def test_rename_custom(self): + """ custom field names must be start with 'x_' """ + field = self.create_field('x_foo') + with self.assertRaises(ValidationError): + field.name = 'foo' + + def test_create_valid(self): + """ field names must be valid pg identifiers """ + with self.assertRaises(ValidationError): + self.create_field('x_foo bar') + + def test_rename_valid(self): + """ field names must be valid pg identifiers """ + field = self.create_field('x_foo') + with self.assertRaises(ValidationError): + field.name = 'x_foo bar' + + def test_create_unique(self): + """ one cannot create two fields with the same name on a given model """ + self.create_field('x_foo') + with self.assertRaises(ValidationError): + self.create_field('x_foo') + + def test_rename_unique(self): + """ one cannot create two fields with the same name on a given model """ + field1 = self.create_field('x_foo') + field2 = self.create_field('x_bar') + with self.assertRaises(ValidationError): + field2.name = field1.name + + def test_remove_without_view(self): + """ try removing a custom field that does not occur in views """ + field = self.create_field('x_foo') + field.unlink() + + def test_rename_without_view(self): + """ try renaming a custom field that does not occur in views """ + field = self.create_field('x_foo') + field.name = 'x_bar' + + def test_remove_with_view(self): + """ try removing a custom field that occurs in a view """ + field = self.create_field('x_foo') + self.create_view('x_foo') + + # try to delete the field, this should fail but not modify the registry + with self.assertRaises(UserError): + field.unlink() + self.assertIn('x_foo', self.env[self.MODEL]._fields) + + def test_rename_with_view(self): + """ try renaming a custom field that occurs in a view """ + field = self.create_field('x_foo') + self.create_view('x_foo') + + # try to delete the field, this should fail but not modify the registry + with self.assertRaises(UserError): + field.name = 'x_bar' + self.assertIn('x_foo', self.env[self.MODEL]._fields) diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv index 03ec40061ce..4e136481512 100644 --- a/odoo/addons/test_new_api/ir.model.access.csv +++ b/odoo/addons/test_new_api/ir.model.access.csv @@ -17,3 +17,4 @@ access_test_new_api_company,access_test_new_api_company,model_test_new_api_compa access_test_new_api_company_attr,access_test_new_api_company_attr,model_test_new_api_company_attr,,1,1,1,1 access_test_new_api_sparse,access_test_new_api_sparse,model_test_new_api_sparse,,1,1,1,1 access_test_new_api_compute_inverse,access_test_new_api_compute_inverse,model_test_new_api_compute_inverse,,1,1,1,1 +access_test_new_api_recursive,access_test_new_api_recursive,model_test_new_api_recursive,,1,1,1,1 diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index f719c696494..ad7092159a4 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -360,3 +360,19 @@ class Sparse(models.Model): char = fields.Char(sparse='data') selection = fields.Selection([('one', 'One'), ('two', 'Two')], sparse='data') partner = fields.Many2one('res.partner', sparse='data') + + +class ComputeRecursive(models.Model): + _name = 'test_new_api.recursive' + + name = fields.Char(required=True) + parent = fields.Many2one('test_new_api.recursive') + display_name = fields.Char(compute='_compute_display_name', store=True) + + @api.depends('name', 'parent.display_name') + def _compute_display_name(self): + for rec in self: + if rec.parent: + rec.display_name = rec.parent.display_name + " / " + rec.name + else: + rec.display_name = rec.name diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 1326eb3da39..9deaee1a5f0 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -179,6 +179,29 @@ class TestFields(common.TransactionCase): cath.parent = finn self.assertEqual(ewan.display_name, "Gabriel / Finnley / Catherine / Ewan") + def test_12_recursive_recompute(self): + """ test recomputation on recursively dependent field """ + a = self.env['test_new_api.recursive'].create({'name': 'A'}) + b = self.env['test_new_api.recursive'].create({'name': 'B', 'parent': a.id}) + c = self.env['test_new_api.recursive'].create({'name': 'C', 'parent': b.id}) + d = self.env['test_new_api.recursive'].create({'name': 'D', 'parent': c.id}) + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'A / B') + self.assertEqual(c.display_name, 'A / B / C') + self.assertEqual(d.display_name, 'A / B / C / D') + + b.parent = False + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'B') + self.assertEqual(c.display_name, 'B / C') + self.assertEqual(d.display_name, 'B / C / D') + + b.name = 'X' + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'X') + self.assertEqual(c.display_name, 'X / C') + self.assertEqual(d.display_name, 'X / C / D') + def test_12_cascade(self): """ test computed field depending on computed field """ message = self.env.ref('test_new_api.message_0_0') diff --git a/odoo/fields.py b/odoo/fields.py index a5404e3c367..b14be35ba54 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -708,6 +708,7 @@ class Field(object): model._field_triggers.add(field, (self, path_str)) elif path: self.recursive = True + model._field_triggers.add(field, (self, '.'.join(path))) ############################################################################ # diff --git a/odoo/models.py b/odoo/models.py index c6e265e4f86..56923cd1558 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -669,7 +669,7 @@ class BaseModel(object): field = cls._fields.get(name) if not field: _logger.warning("method %s.%s: @constrains parameter %r is not a field name", cls._name, attr, name) - if not (field.store or field.inverse): + elif not (field.store or field.inverse): _logger.warning("method %s.%s: @constrains parameter %r is not writeable", cls._name, attr, name) methods.append(func) @@ -4342,7 +4342,8 @@ class BaseModel(object): """ self.ensure_one() vals = self.copy_data(default)[0] - new = self.create(vals) + # To avoid to create a translation in the lang of the user, copy_translation will do it + new = self.with_context(lang=None).create(vals) self.copy_translations(new) return new diff --git a/odoo/modules/graph.py b/odoo/modules/graph.py index aaf90ae8443..ff2f99188fe 100644 --- a/odoo/modules/graph.py +++ b/odoo/modules/graph.py @@ -80,7 +80,7 @@ class Graph(dict): info = odoo.modules.module.load_information_from_description_file(module) if info and info['installable']: packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version - else: + elif module != 'studio_customization': _logger.warning('module %s: not installable, skipped', module) dependencies = dict([(p, info['depends']) for p, info in packages]) @@ -112,10 +112,7 @@ class Graph(dict): unmet_deps = filter(lambda p: p not in self, dependencies[package]) _logger.error('module %s: Unmet dependencies: %s', package, ', '.join(unmet_deps)) - result = len(self) - len_graph - if result != len(module_list): - _logger.warning('Some modules were not loaded.') - return result + return len(self) - len_graph def __iter__(self): diff --git a/odoo/modules/module.py b/odoo/modules/module.py index ecf95be088b..d15df6dba59 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -302,7 +302,7 @@ def load_information_from_description_file(module, mod_path=None): :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...) """ if not mod_path: - mod_path = get_module_path(module) + mod_path = get_module_path(module, downloaded=True) manifest_file = module_manifest(mod_path) if manifest_file: # default values for descriptor diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index eea14988eb7..bef1cb1c523 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -876,8 +876,7 @@ class expression(object): # Making search easier when there is a left operand as one2many or many2many elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'): right_ids = comodel.search([('.'.join(path[1:]), operator, right)]).ids - table_ids = model.with_context(active_test=False).search([(path[0], 'in', right_ids)]).ids - leaf.leaf = ('id', 'in', table_ids) + leaf.leaf = (path[0], 'in', right_ids) push(leaf) elif not field.store: @@ -980,15 +979,16 @@ class expression(object): rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 if operator in HIERARCHY_FUNCS: - def _rec_convert(ids): - if comodel == model: - return ids - return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = to_ids(right, comodel) dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel) ids2 = comodel.search(dom).ids - push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model)) + if comodel == model: + push(create_substitution_leaf(leaf, ('id', 'in', ids2), model)) + else: + subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) + # avoid flattening of argument in to_sql() + subquery = cr.mogrify(subquery, [tuple(ids2)]) + push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [])), internal=True)) else: call_null_m2m = True if right is not False: @@ -1014,8 +1014,11 @@ class expression(object): operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False - m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model)) + subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect' + subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) + # avoid flattening of argument in to_sql() + subquery = cr.mogrify(subquery, [tuple(filter(None, res_ids))]) + push(create_substitution_leaf(leaf, ('id', subop, (subquery, [])), internal=True)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'