[IMP] base: Currencies configuration
Improves list; kanban and form view for currencies and currency rates. Add checks to prevent changing/adding rates when accounting entries exists on dates concerned by those rates. Task: 2450747
This commit is contained in:
@@ -12,6 +12,7 @@ The kernel of Odoo, needed for all installation.
|
||||
""",
|
||||
'depends': [],
|
||||
'data': [
|
||||
'data/res_bank.xml',
|
||||
'data/res.lang.csv',
|
||||
'data/res_lang_data.xml',
|
||||
'data/res_partner_data.xml',
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="res_bank_1" model="res.bank">
|
||||
<field name="name">Reserve</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
<record id="rateUSDbis" model="res.currency.rate">
|
||||
<field name="rate">1.5289</field>
|
||||
<field name="currency_id" ref="USD"/>
|
||||
<field eval="time.strftime('%Y-06-06')" name="name"/>
|
||||
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
||||
</record>
|
||||
|
||||
<record forcecreate="0" id="rateVEF" model="res.currency.rate">
|
||||
|
||||
@@ -10,7 +10,6 @@ import re
|
||||
from odoo import api, fields, models, tools, _, Command
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.modules.module import get_resource_path
|
||||
|
||||
from random import randrange
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import logging
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from lxml import etree
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -27,18 +27,25 @@ class Currency(models.Model):
|
||||
|
||||
# Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code.
|
||||
name = fields.Char(string='Currency', size=3, required=True, help="Currency Code (ISO 4217)")
|
||||
full_name = fields.Char(string='Name')
|
||||
symbol = fields.Char(help="Currency sign, to be used when printing amounts.", required=True)
|
||||
rate = fields.Float(compute='_compute_current_rate', string='Current Rate', digits=0,
|
||||
help='The rate of the currency to the currency of rate 1.')
|
||||
inverse_rate = fields.Float(compute='_compute_current_rate', digits=0, readonly=True,
|
||||
help='The currency of rate 1 to the rate of the currency.')
|
||||
rate_string = fields.Char(compute='_compute_current_rate')
|
||||
rate_ids = fields.One2many('res.currency.rate', 'currency_id', string='Rates')
|
||||
rounding = fields.Float(string='Rounding Factor', digits=(12, 6), default=0.01)
|
||||
decimal_places = fields.Integer(compute='_compute_decimal_places', store=True)
|
||||
rounding = fields.Float(string='Rounding Factor', digits=(12, 6), default=0.01,
|
||||
help='Amounts in this currency are rounded off to the nearest multiple of the rounding factor.')
|
||||
decimal_places = fields.Integer(compute='_compute_decimal_places', store=True,
|
||||
help='Decimal places taken into account for operations on amounts in this currency. It is determined by the rounding factor.')
|
||||
active = fields.Boolean(default=True)
|
||||
position = fields.Selection([('after', 'After Amount'), ('before', 'Before Amount')], default='after',
|
||||
string='Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.")
|
||||
date = fields.Date(compute='_compute_date')
|
||||
currency_unit_label = fields.Char(string="Currency Unit", help="Currency Unit Name")
|
||||
currency_subunit_label = fields.Char(string="Currency Subunit", help="Currency Subunit Name")
|
||||
is_current_company_currency = fields.Boolean(compute='_compute_is_current_company_currency')
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_name', 'unique (name)', 'The currency code must be unique!'),
|
||||
@@ -61,14 +68,25 @@ class Currency(models.Model):
|
||||
currency_rates = dict(self._cr.fetchall())
|
||||
return currency_rates
|
||||
|
||||
@api.depends_context('company')
|
||||
def _compute_is_current_company_currency(self):
|
||||
for currency in self:
|
||||
currency.is_current_company_currency = self.env.company.currency_id == currency
|
||||
|
||||
@api.depends('rate_ids.rate')
|
||||
def _compute_current_rate(self):
|
||||
date = self._context.get('date') or fields.Date.today()
|
||||
company = self.env['res.company'].browse(self._context.get('company_id')) or self.env.company
|
||||
# the subquery selects the last rate before 'date' for the given currency/company
|
||||
currency_rates = self._get_rates(company, date)
|
||||
last_rate = self.env['res.currency.rate']._get_last_rates_for_companies(company)
|
||||
for currency in self:
|
||||
currency.rate = currency_rates.get(currency.id) or 1.0
|
||||
currency.rate = (currency_rates.get(currency.id) or 1.0) / last_rate[company]
|
||||
currency.inverse_rate = 1 / currency.rate
|
||||
if currency != company.currency_id:
|
||||
currency.rate_string = '1 %s = %s %s' % (company.currency_id.name, currency.rate, currency.name)
|
||||
else:
|
||||
currency.rate_string = ''
|
||||
|
||||
@api.depends('rounding')
|
||||
def _compute_decimal_places(self):
|
||||
@@ -229,6 +247,20 @@ class Currency(models.Model):
|
||||
JOIN res_company c ON (r.company_id is null or r.company_id = c.id)
|
||||
"""
|
||||
|
||||
@api.model
|
||||
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
|
||||
result = super(Currency, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
|
||||
if view_type in ('tree', 'form'):
|
||||
currency_name = (self.env['res.company'].browse(self._context.get('company_id')) or self.env.company).currency_id.name
|
||||
doc = etree.XML(result['arch'])
|
||||
for field in [['company_rate', _('Unit per %s', currency_name)],
|
||||
['inverse_company_rate', _('%s per Unit', currency_name)]]:
|
||||
node = doc.xpath("//tree//field[@name='%s']" % field[0])
|
||||
if node:
|
||||
node[0].set('string', field[1])
|
||||
result['arch'] = etree.tostring(doc, encoding='unicode')
|
||||
return result
|
||||
|
||||
|
||||
class CurrencyRate(models.Model):
|
||||
_name = "res.currency.rate"
|
||||
@@ -237,8 +269,26 @@ class CurrencyRate(models.Model):
|
||||
|
||||
name = fields.Date(string='Date', required=True, index=True,
|
||||
default=lambda self: fields.Date.today())
|
||||
rate = fields.Float(digits=0, compute="_compute_rate", store=True, readonly=False, group_operator="avg",
|
||||
help='The rate of the currency to the currency of rate 1')
|
||||
rate = fields.Float(
|
||||
digits=0,
|
||||
group_operator="avg",
|
||||
help='The rate of the currency to the currency of rate 1',
|
||||
string='Technical Rate'
|
||||
)
|
||||
company_rate = fields.Float(
|
||||
digits=0,
|
||||
compute="_compute_company_rate",
|
||||
inverse="_inverse_company_rate",
|
||||
group_operator="avg",
|
||||
help="The currency of rate 1 to the rate of the currency.",
|
||||
)
|
||||
inverse_company_rate = fields.Float(
|
||||
digits=0,
|
||||
compute="_compute_inverse_company_rate",
|
||||
inverse="_inverse_inverse_company_rate",
|
||||
group_operator="avg",
|
||||
help="The currency of rate 1 to the rate of the currency.",
|
||||
)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, required=True, ondelete="cascade")
|
||||
company_id = fields.Many2one('res.company', string='Company',
|
||||
default=lambda self: self.env.company)
|
||||
@@ -248,26 +298,70 @@ class CurrencyRate(models.Model):
|
||||
('currency_rate_check', 'CHECK (rate>0)', 'The currency rate must be strictly positive.'),
|
||||
]
|
||||
|
||||
def _sanitize_vals(self, vals):
|
||||
if 'inverse_company_rate' in vals and ('company_rate' in vals or 'rate' in vals):
|
||||
del vals['inverse_company_rate']
|
||||
if 'company_rate' in vals and 'rate' in vals:
|
||||
del vals['company_rate']
|
||||
return vals
|
||||
|
||||
def write(self, vals):
|
||||
return super().write(self._sanitize_vals(vals))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
return super().create([self._sanitize_vals(vals) for vals in vals_list])
|
||||
|
||||
def _get_latest_rate(self):
|
||||
return self.search([
|
||||
('currency_id', '=', self.currency_id.id),
|
||||
('company_id', '=', self.company_id.id or self.env.company.id),
|
||||
('name', '<=', self.name or fields.Date.today()),
|
||||
], order="name desc", limit=1)
|
||||
return self.currency_id.rate_ids.filtered(lambda x: (
|
||||
x.rate
|
||||
and x.company_id == (self.company_id or self.env.company)
|
||||
and x.name < (self.name or fields.Date.today())
|
||||
)).sorted('name')[-1:]
|
||||
|
||||
def _get_last_rates_for_companies(self, companies):
|
||||
return {
|
||||
company: company.currency_id.rate_ids.filtered(lambda x: (
|
||||
x.rate
|
||||
and x.company_id == company or not x.company_id
|
||||
)).sorted('name')[-1:].rate or 1
|
||||
for company in companies
|
||||
}
|
||||
|
||||
@api.depends('currency_id', 'company_id', 'name')
|
||||
def _compute_rate(self):
|
||||
for currency_rate in self:
|
||||
latest_rate = currency_rate._get_latest_rate()
|
||||
currency_rate.rate = latest_rate.rate if latest_rate else 1.0
|
||||
currency_rate.rate = currency_rate.rate or self._get_latest_rate().rate or 1.0
|
||||
|
||||
@api.onchange('rate')
|
||||
@api.depends('rate', 'name', 'currency_id', 'company_id', 'currency_id.rate_ids.rate')
|
||||
@api.depends_context('company')
|
||||
def _compute_company_rate(self):
|
||||
last_rate = self.env['res.currency.rate']._get_last_rates_for_companies(self.company_id | self.env.company)
|
||||
for currency_rate in self:
|
||||
company = currency_rate.company_id or self.env.company
|
||||
currency_rate.company_rate = (currency_rate.rate or self._get_latest_rate().rate or 1.0) / last_rate[company]
|
||||
|
||||
@api.onchange('company_rate')
|
||||
def _inverse_company_rate(self):
|
||||
last_rate = self.env['res.currency.rate']._get_last_rates_for_companies(self.company_id | self.env.company)
|
||||
for currency_rate in self:
|
||||
company = currency_rate.company_id or self.env.company
|
||||
currency_rate.rate = currency_rate.company_rate * last_rate[company]
|
||||
|
||||
@api.depends('company_rate')
|
||||
def _compute_inverse_company_rate(self):
|
||||
for currency_rate in self:
|
||||
currency_rate.inverse_company_rate = 1.0 / currency_rate.company_rate
|
||||
|
||||
@api.onchange('inverse_company_rate')
|
||||
def _inverse_inverse_company_rate(self):
|
||||
for currency_rate in self:
|
||||
currency_rate.company_rate = 1.0 / currency_rate.inverse_company_rate
|
||||
|
||||
@api.onchange('company_rate')
|
||||
def _onchange_rate_warning(self):
|
||||
if not self.currency_id.id:
|
||||
return
|
||||
|
||||
latest_rate = self._get_latest_rate()
|
||||
if latest_rate and latest_rate.rate:
|
||||
if latest_rate:
|
||||
diff = (latest_rate.rate - self.rate) / latest_rate.rate
|
||||
if abs(diff) > 0.2:
|
||||
return {
|
||||
@@ -298,3 +392,20 @@ class CurrencyRate(models.Model):
|
||||
name = ''
|
||||
operator = 'ilike'
|
||||
return super(CurrencyRate, self)._name_search(name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
|
||||
|
||||
@api.model
|
||||
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
|
||||
result = super(CurrencyRate, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
|
||||
if view_type in ('tree'):
|
||||
names = {
|
||||
'company_currency_name': (self.env['res.company'].browse(self._context.get('company_id')) or self.env.company).currency_id.name,
|
||||
'rate_currency_name': self.env['res.currency'].browse(self._context.get('active_id')).name or 'Unit',
|
||||
}
|
||||
doc = etree.XML(result['arch'])
|
||||
for field in [['company_rate', _('%(rate_currency_name)s per %(company_currency_name)s', **names)],
|
||||
['inverse_company_rate', _('%(company_currency_name)s per %(rate_currency_name)s', **names)]]:
|
||||
node = doc.xpath("//tree//field[@name='%s']" % field[0])
|
||||
if node:
|
||||
node[0].set('string', field[1])
|
||||
result['arch'] = etree.tostring(doc, encoding='unicode')
|
||||
return result
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Currency Rates" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="rate" digits="[12,12]"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="company_rate" digits="[12,12]"/>
|
||||
<field name="inverse_company_rate" digits="[12,12]"/>
|
||||
<field name="rate" digits="[12,12]" optional="hide"/>
|
||||
<field name="write_date" optional="hide"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -32,13 +35,14 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="rate" digits="[12,12]"/>
|
||||
<field name="rate" digits="[12,12]" groups="base.group_no_one"/>
|
||||
<field name="company_rate" digits="[12,12]"/>
|
||||
<field name="inverse_company_rate" digits="[12,12]"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@@ -46,12 +50,15 @@
|
||||
</record>
|
||||
|
||||
<record id="act_view_currency_rates" model="ir.actions.act_window">
|
||||
<field name="name">Currency Rates</field>
|
||||
<field name="name">Show Currency Rates</field>
|
||||
<field name="res_model">res.currency.rate</field>
|
||||
<field name='view_mode'>tree,form</field>
|
||||
<field name='view_id' ref='view_currency_rate_tree'/>
|
||||
<field name="domain">[('currency_id','=', active_id)]</field>
|
||||
<field name="context">{'default_currency_id': active_id}</field>
|
||||
<!-- <field name="target">fullscreen</field> -->
|
||||
<field name="binding_model_id" ref="model_res_currency"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
</record>
|
||||
|
||||
<record id="view_currency_search" model="ir.ui.view">
|
||||
@@ -73,9 +80,10 @@
|
||||
<tree string="Currencies" decoration-muted="(not active)">
|
||||
<field name="name"/>
|
||||
<field name="symbol"/>
|
||||
<field name="rate_ids" invisible="1"/>
|
||||
<field name="date"/>
|
||||
<field name="full_name" string="Name" optional="show"/>
|
||||
<field name="date" string="Last Update"/>
|
||||
<field name="rate" digits="[12,6]"/>
|
||||
<field name="inverse_rate" digits="[12,6]" optional="hide"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -88,21 +96,26 @@
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="symbol"/>
|
||||
<field name="full_name"/>
|
||||
<field name="active"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div class="row mb4">
|
||||
<div class="col-6">
|
||||
<strong><span><t t-esc="record.name.value"/></span></strong>
|
||||
<div class="col-2">
|
||||
<h3><t t-esc="record.name.value"/></h3>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<div class="col-5">
|
||||
<span class="badge badge-pill"><t t-esc="record.symbol.value"/></span>
|
||||
</div>
|
||||
<div class="col-5 text-right">
|
||||
<t t-if="! record.active.raw_value"><span class="badge badge-pill bg-light border">inactive</span></t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<strong><span>Current Rate: </span></strong>
|
||||
<field name="rate" digits="[12,6]"/>
|
||||
<div><field name="rate_string"/></div>
|
||||
<t t-if="record.date.raw_value"><div>Last update: <field name="date"/></div></t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,15 +130,19 @@
|
||||
<field name="model">res.currency</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Currency">
|
||||
<field name="is_current_company_currency" invisible="1"/>
|
||||
<div class="oe_edit_only alert alert-info text-center" role="alert" groups="base.group_no_one">
|
||||
You cannot reduce the number of decimal places of a currency already used on an accounting entry.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info text-center" role="alert" attrs="{'invisible': [('is_current_company_currency','=',False)]}">
|
||||
This is your company's currency.
|
||||
</div>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button class="oe_stat_button" string="Rates" type="action" name="%(act_view_currency_rates)d"
|
||||
icon="fa-money" widget="statinfo" attrs="{'invisible': [('active', '=', False)]}"/>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="rate" digits="[12,12]"/>
|
||||
<field name="full_name" string="Name"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
<group>
|
||||
@@ -134,10 +151,6 @@
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<div class="oe_edit_only alert alert-info" role="alert">
|
||||
You won't be able to reduce the number of decimal places of a currency which has already been used to make accounting entries.
|
||||
</div>
|
||||
|
||||
<group groups="base.group_no_one">
|
||||
<group string="Price Accuracy">
|
||||
<field name="rounding"/>
|
||||
@@ -149,10 +162,25 @@
|
||||
<field name="position"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook class="o_currency_rate_list" attrs="{'invisible': [('is_current_company_currency','=',True)]}">
|
||||
<page string="Rates" name="rates">
|
||||
<field name="rate_ids" widget="one2many">
|
||||
<tree string="Rates" editable="top" limit="25">
|
||||
<field name="name"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="company_rate" digits="[12,12]"/>
|
||||
<field name="inverse_company_rate" digits="[12,12]"/>
|
||||
<field name="rate" digits="[12,12]" optional="hide"/>
|
||||
<field name="write_date" optional="hide"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_currency_form" model="ir.actions.act_window">
|
||||
<field name="name">Currencies</field>
|
||||
<field name="res_model">res.currency</field>
|
||||
|
||||
Reference in New Issue
Block a user