[MERGE] forward port branch saas-12 up to 00f4547
This commit is contained in:
@@ -773,6 +773,9 @@ class AccountTax(models.Model):
|
||||
else:
|
||||
total_included += tax_amount
|
||||
|
||||
# Keep base amount used for the current tax
|
||||
tax_base = base
|
||||
|
||||
if tax.include_base_amount:
|
||||
base += tax_amount
|
||||
|
||||
@@ -780,6 +783,7 @@ class AccountTax(models.Model):
|
||||
'id': tax.id,
|
||||
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
|
||||
'amount': tax_amount,
|
||||
'base': tax_base,
|
||||
'sequence': tax.sequence,
|
||||
'account_id': tax.account_id.id,
|
||||
'refund_account_id': tax.refund_account_id.id,
|
||||
|
||||
@@ -366,6 +366,7 @@ class AccountBankStatementLine(models.Model):
|
||||
journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True)
|
||||
amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.")
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.")
|
||||
state = fields.Selection(related='statement_id.state' , string='Status', readonly=True)
|
||||
move_name = fields.Char(string='Journal Entry Name', readonly=True,
|
||||
default=False, copy=False,
|
||||
help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.")
|
||||
|
||||
@@ -12,6 +12,9 @@ from odoo.tools.misc import formatLang
|
||||
from odoo.exceptions import UserError, RedirectWarning, ValidationError
|
||||
|
||||
import odoo.addons.decimal_precision as dp
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# mapping invoice type to journal type
|
||||
TYPE2JOURNAL = {
|
||||
@@ -592,6 +595,25 @@ class AccountInvoice(models.Model):
|
||||
else:
|
||||
return self.env.ref('account.invoice_form').id
|
||||
|
||||
def _prepare_tax_line_vals(self, line, tax):
|
||||
""" Prepare values to create an account.invoice.tax line
|
||||
|
||||
The line parameter is an account.invoice.line, and the
|
||||
tax parameter is the output of account.tax.compute_all().
|
||||
"""
|
||||
vals = {
|
||||
'invoice_id': self.id,
|
||||
'name': tax['name'],
|
||||
'tax_id': tax['id'],
|
||||
'amount': tax['amount'],
|
||||
'base': tax['base'],
|
||||
'manual': False,
|
||||
'sequence': tax['sequence'],
|
||||
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
|
||||
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
|
||||
}
|
||||
return vals
|
||||
|
||||
@api.multi
|
||||
def get_taxes_values(self):
|
||||
tax_grouped = {}
|
||||
@@ -599,16 +621,7 @@ class AccountInvoice(models.Model):
|
||||
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes']
|
||||
for tax in taxes:
|
||||
val = {
|
||||
'invoice_id': self.id,
|
||||
'name': tax['name'],
|
||||
'tax_id': tax['id'],
|
||||
'amount': tax['amount'],
|
||||
'manual': False,
|
||||
'sequence': tax['sequence'],
|
||||
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
|
||||
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
|
||||
}
|
||||
val = self._prepare_tax_line_vals(line, tax)
|
||||
|
||||
# If the taxes generate moves on the same financial account as the invoice line,
|
||||
# propagate the analytic account from the invoice line to the tax line.
|
||||
@@ -623,6 +636,7 @@ class AccountInvoice(models.Model):
|
||||
tax_grouped[key] = val
|
||||
else:
|
||||
tax_grouped[key]['amount'] += val['amount']
|
||||
tax_grouped[key]['base'] += val['base']
|
||||
return tax_grouped
|
||||
|
||||
@api.multi
|
||||
@@ -1332,15 +1346,19 @@ class AccountInvoiceTax(models.Model):
|
||||
_order = 'sequence'
|
||||
|
||||
def _compute_base_amount(self):
|
||||
tax_grouped = {}
|
||||
for invoice in self.mapped('invoice_id'):
|
||||
tax_grouped[invoice.id] = invoice.get_taxes_values()
|
||||
for tax in self:
|
||||
base = 0.0
|
||||
for line in tax.invoice_id.invoice_line_ids:
|
||||
if tax.tax_id in line.invoice_line_tax_ids:
|
||||
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
base += (line.invoice_line_tax_ids - tax.tax_id).compute_all(
|
||||
price_unit, line.invoice_id.currency_id, line.quantity, line.product_id, line.invoice_id.partner_id
|
||||
)['base']
|
||||
tax.base = base
|
||||
key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({
|
||||
'tax_id': tax.tax_id.id,
|
||||
'account_id': tax.account_id.id,
|
||||
'account_analytic_id': tax.account_analytic_id.id,
|
||||
})
|
||||
if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]:
|
||||
tax.base = tax_grouped[tax.invoice_id.id][key]['base']
|
||||
else:
|
||||
_logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name)
|
||||
|
||||
invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True)
|
||||
name = fields.Char(string='Tax Description', required=True)
|
||||
|
||||
@@ -41,3 +41,65 @@ class TestAccountSupplierInvoice(AccountingTestCase):
|
||||
#I cancel the account move which is in posted state and verifies that it gives warning message
|
||||
with self.assertRaises(Warning):
|
||||
invoice.move_id.button_cancel()
|
||||
|
||||
def test_supplier_invoice2(self):
|
||||
tax_fixed = self.env['account.tax'].create({
|
||||
'sequence': 10,
|
||||
'name': 'Tax 10.0 (Fixed)',
|
||||
'amount': 10.0,
|
||||
'amount_type': 'fixed',
|
||||
'include_base_amount': True,
|
||||
})
|
||||
tax_percent_included_base_incl = self.env['account.tax'].create({
|
||||
'sequence': 20,
|
||||
'name': 'Tax 50.0% (Percentage of Price Tax Included)',
|
||||
'amount': 50.0,
|
||||
'amount_type': 'division',
|
||||
'include_base_amount': True,
|
||||
})
|
||||
tax_percentage = self.env['account.tax'].create({
|
||||
'sequence': 30,
|
||||
'name': 'Tax 20.0% (Percentage of Price)',
|
||||
'amount': 20.0,
|
||||
'amount_type': 'percent',
|
||||
'include_base_amount': False,
|
||||
})
|
||||
analytic_account = self.env['account.analytic.account'].create({
|
||||
'name': 'test account',
|
||||
})
|
||||
|
||||
# Should be changed by automatic on_change later
|
||||
invoice_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1).id
|
||||
invoice_line_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1).id
|
||||
|
||||
invoice = self.env['account.invoice'].create({'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'account_id': invoice_account,
|
||||
'type': 'in_invoice',
|
||||
})
|
||||
|
||||
invoice_line = self.env['account.invoice.line'].create({'product_id': self.env.ref('product.product_product_4').id,
|
||||
'quantity': 5.0,
|
||||
'price_unit': 100.0,
|
||||
'invoice_id': invoice.id,
|
||||
'name': 'product that cost 100',
|
||||
'account_id': invoice_line_account,
|
||||
'invoice_line_tax_ids': [(6, 0, [tax_fixed.id, tax_percent_included_base_incl.id, tax_percentage.id])],
|
||||
'account_analytic_id': analytic_account.id,
|
||||
})
|
||||
invoice.compute_taxes()
|
||||
|
||||
# check that Initially supplier bill state is "Draft"
|
||||
self.assertTrue((invoice.state == 'draft'), "Initially vendor bill state is Draft")
|
||||
|
||||
#change the state of invoice to open by clicking Validate button
|
||||
invoice.signal_workflow('invoice_open')
|
||||
|
||||
# Check if amount and corresponded base is correct for all tax scenarios given on a computational base
|
||||
# Keep in mind that tax amount can be changed by the user at any time before validating (based on the invoice and tax laws applicable)
|
||||
invoice_tax = invoice.tax_line_ids.sorted(key=lambda r: r.sequence)
|
||||
self.assertEquals(invoice_tax.mapped('amount'), [50.0, 550.0, 220.0])
|
||||
self.assertEquals(invoice_tax.mapped('base'), [500.0, 550.0, 1100.0])
|
||||
|
||||
#I cancel the account move which is in posted state and verifies that it gives warning message
|
||||
with self.assertRaises(Warning):
|
||||
invoice.move_id.button_cancel()
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
<span class="fa fa-copy fa-4x"></span>
|
||||
<h3><strong>Contracts & Subscriptions</strong></h3>
|
||||
<p>Manage time & material contracts or fixed-price recurring subscriptions.</p>
|
||||
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account_analytic_analysis')"
|
||||
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'sale_contract')"
|
||||
class="btn odoo_purple">
|
||||
<span class="fa fa-arrow-circle-o-down"/> Install Now
|
||||
</a>
|
||||
|
||||
@@ -153,7 +153,7 @@ class AccountAssetAsset(models.Model):
|
||||
else:
|
||||
if self.method == 'linear':
|
||||
amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
|
||||
if self.prorata and self.category_id.type == 'purchase':
|
||||
if self.prorata:
|
||||
amount = amount_to_depr / self.method_number
|
||||
if sequence == 1:
|
||||
days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
|
||||
@@ -174,7 +174,7 @@ class AccountAssetAsset(models.Model):
|
||||
while depreciation_date <= end_date:
|
||||
depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period)
|
||||
undone_dotation_number += 1
|
||||
if self.prorata and self.category_id.type == 'purchase':
|
||||
if self.prorata:
|
||||
undone_dotation_number += 1
|
||||
return undone_dotation_number
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
<attribute name="options">{'reload_on_button': true}</attribute>
|
||||
</field>
|
||||
<xpath expr="//field[@name='bank_account_id']" position="after">
|
||||
<button name="button_cancel_reconciliation" attrs="{'invisible': [('journal_entry_ids', '=', [])]}" string="Cancel" type="object" icon="fa-undo text-warning"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<button name="button_cancel_reconciliation" attrs="{'invisible': ['|',('journal_entry_ids', '=', []), ('state', '=', 'confirm')]}" string="Cancel" type="object" icon="fa fa-undo text-warning"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -198,7 +198,14 @@ class BaseActionRule(models.Model):
|
||||
|
||||
# subscribe followers
|
||||
if self.act_followers and hasattr(records, 'message_subscribe'):
|
||||
records.message_subscribe(self.act_followers.ids)
|
||||
followers = self.env['mail.followers'].sudo().search(
|
||||
[('res_model', '=', records._name),
|
||||
('res_id', 'in', records.ids),
|
||||
('partner_id', 'in', self.act_followers.ids),
|
||||
]
|
||||
)
|
||||
if not len(followers) == len(self.act_followers):
|
||||
records.message_subscribe(self.act_followers.ids)
|
||||
|
||||
# execute server actions
|
||||
if self.server_action_ids:
|
||||
|
||||
@@ -1372,7 +1372,7 @@ class Meeting(models.Model):
|
||||
if not self._context.get('dont_notify'):
|
||||
if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'):
|
||||
partners_to_notify = meeting.partner_ids.ids
|
||||
event_attendees_changes = attendees_create and attendees_create[real_ids[0]]
|
||||
event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]]
|
||||
if event_attendees_changes:
|
||||
partners_to_notify.append(event_attendees_changes['removed_partners'].ids)
|
||||
self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify)
|
||||
|
||||
@@ -213,5 +213,5 @@ class Employee(models.Model):
|
||||
('date_from', '<=', today_end),
|
||||
('date_to', '>=', today_start),
|
||||
('type', '=', 'remove')
|
||||
], ['employee_id'])
|
||||
])
|
||||
return [('id', 'in', holidays.mapped('employee_id').ids)]
|
||||
|
||||
@@ -410,6 +410,10 @@ var PosDB = core.Class.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// Only necessary when we store a new, validated order. Orders
|
||||
// that where already stored should already have been removed.
|
||||
this.remove_unpaid_order(order);
|
||||
|
||||
orders.push({id: order_id, data: order});
|
||||
this.save('orders',orders);
|
||||
return order_id;
|
||||
|
||||
@@ -1895,7 +1895,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
}
|
||||
|
||||
// if the change is too large, it's probably an input error, make the user confirm.
|
||||
if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
|
||||
if (!force_validation && order.get_total_with_tax() > 0 && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
|
||||
this.gui.show_popup('confirm',{
|
||||
title: _t('Please Confirm Large Amount'),
|
||||
body: _t('Are you sure that the customer wants to pay') +
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import models, fields, api
|
||||
@@ -29,7 +29,7 @@ class pos_cache(models.Model):
|
||||
prod_ctx = prod_ctx.sudo(self.compute_user_id.id)
|
||||
res = prod_ctx.read(self.get_product_fields())
|
||||
datas = {
|
||||
'cache': cPickle.dumps(res),
|
||||
'cache': base64.encodestring(cPickle.dumps(res)),
|
||||
}
|
||||
|
||||
self.write(datas)
|
||||
@@ -49,7 +49,8 @@ class pos_cache(models.Model):
|
||||
self.product_fields = str(fields)
|
||||
self.refresh_cache()
|
||||
|
||||
return cPickle.loads(self.cache)
|
||||
cache = base64.decodestring(self.cache)
|
||||
return cPickle.loads(cache)
|
||||
|
||||
|
||||
class pos_config(models.Model):
|
||||
|
||||
@@ -194,16 +194,16 @@ class PurchaseOrder(models.Model):
|
||||
taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id).ids
|
||||
|
||||
# Compute quantity and price_unit
|
||||
if requisition.type_id.quantity_copy != 'copy':
|
||||
product_qty = 0
|
||||
price_unit = line.price_unit
|
||||
elif line.product_uom_id != line.product_id.uom_po_id:
|
||||
if line.product_uom_id != line.product_id.uom_po_id:
|
||||
product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id)
|
||||
price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id)
|
||||
else:
|
||||
product_qty = line.product_qty
|
||||
price_unit = line.price_unit
|
||||
|
||||
if requisition.type_id.quantity_copy != 'copy':
|
||||
product_qty = 0
|
||||
|
||||
# Compute price_unit in appropriate currency
|
||||
if requisition.company_id.currency_id != currency:
|
||||
price_unit = requisition.company_id.currency_id.compute(price_unit, currency)
|
||||
@@ -258,6 +258,24 @@ class PurchaseOrder(models.Model):
|
||||
return result
|
||||
|
||||
|
||||
class PurchaseOrderLine(models.Model):
|
||||
_inherit = "purchase.order.line"
|
||||
|
||||
@api.onchange('product_qty', 'product_uom')
|
||||
def _onchange_quantity(self):
|
||||
res = super(PurchaseOrderLine, self)._onchange_quantity()
|
||||
if self.order_id.requisition_id:
|
||||
for line in self.order_id.requisition_id.line_ids:
|
||||
if line.product_id == self.product_id:
|
||||
if line.product_uom_id != self.product_uom:
|
||||
self.price_unit = self.env['product.uom']._compute_price(
|
||||
line.product_uom_id.id, line.price_unit, to_uom_id=self.product_uom.id)
|
||||
else:
|
||||
self.price_unit = line.price_unit
|
||||
break
|
||||
return res
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
|
||||
@@ -430,6 +430,7 @@ var FormWidget = Widget.extend(InvisibilityChangerMixin, {
|
||||
this.process_modifiers();
|
||||
this._super();
|
||||
this.$el.addClass(this.node.attrs["class"] || "");
|
||||
this.$el.attr('style', this.node.attrs.style);
|
||||
},
|
||||
destroy: function() {
|
||||
$.fn.tooltip('destroy');
|
||||
|
||||
@@ -139,6 +139,24 @@ class TestViewSaving(common.TransactionCase):
|
||||
Company = self.env['res.company']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
# create a view with an xmlid, like the file import would
|
||||
with self.env.norecompute():
|
||||
self.view_id = View.create(self.cr, self.uid, {
|
||||
'name': "Test View",
|
||||
'type': 'qweb',
|
||||
'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8')
|
||||
})
|
||||
imd = self.env['ir.model.data'].create({
|
||||
'module': 'website',
|
||||
'name': 'test_view',
|
||||
'model': 'ir.ui.view',
|
||||
'res_id': self.view_id,
|
||||
})
|
||||
|
||||
# the xml_id of the view should not be flagged as 'noupdate'
|
||||
self.assertEqual(View.browse(self.cr, self.uid, self.view_id).model_data_id, imd)
|
||||
self.assertFalse(imd.noupdate)
|
||||
|
||||
replacement = ET.tostring(h.DIV(
|
||||
h.H3("Column 2"),
|
||||
h.UL(
|
||||
@@ -149,6 +167,9 @@ class TestViewSaving(common.TransactionCase):
|
||||
), encoding='utf-8')
|
||||
self.view_id.save(value=replacement, xpath='/div/div[2]')
|
||||
|
||||
# the xml_id of the view should be flagged as 'noupdate'
|
||||
self.assertTrue(imd.noupdate)
|
||||
|
||||
company = Company.browse(1)
|
||||
self.assertEqual(company.name, "Acme Corporation")
|
||||
self.assertEqual(company.phone, "+12 3456789")
|
||||
|
||||
@@ -229,10 +229,11 @@ actual arch.
|
||||
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
|
||||
view_wo_lang.arch = view.arch_base
|
||||
|
||||
@api.depends('write_date')
|
||||
def _compute_model_data_id(self):
|
||||
# get the last ir_model_data record corresponding to self
|
||||
# get the first ir_model_data record corresponding to self
|
||||
domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
|
||||
for data in self.env['ir.model.data'].search_read(domain, ['res_id']):
|
||||
for data in self.env['ir.model.data'].search_read(domain, ['res_id'], order='id desc'):
|
||||
view = self.browse(data['res_id'])
|
||||
view.model_data_id = data['id']
|
||||
|
||||
@@ -343,7 +344,15 @@ actual arch.
|
||||
values['name'] = "%s %s" % (values.get('model'), values['type'])
|
||||
|
||||
self.clear_caches()
|
||||
return super(View, self).create(self._compute_defaults(values))
|
||||
|
||||
if 'install_mode_data' in self._context:
|
||||
# the view is created from a data file by installing a module; delay
|
||||
# the recomputation of field 'model_data_id' until its xmlid record
|
||||
# is created
|
||||
with self.env.norecompute():
|
||||
return super(View, self).create(self._compute_defaults(values))
|
||||
else:
|
||||
return super(View, self).create(self._compute_defaults(values))
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
|
||||
@@ -12,6 +12,14 @@ from odoo.tests import common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class ViewXMLID(common.TransactionCase):
|
||||
def test_model_data_id(self):
|
||||
""" Check whether views know their xmlid record. """
|
||||
view = self.env.ref('base.view_company_form')
|
||||
self.assertTrue(view)
|
||||
self.assertTrue(view.model_data_id)
|
||||
self.assertEqual(view.model_data_id.complete_name, 'base.view_company_form')
|
||||
|
||||
class ViewCase(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(ViewCase, self).setUp()
|
||||
|
||||
Reference in New Issue
Block a user