summaryrefslogtreecommitdiff
path: root/addons/l10n_fr
diff options
context:
space:
mode:
authorLaurent Smet <las@odoo.com>2019-09-17 10:12:38 +0000
committerFlorian Gilbert <flg@odoo.com>2019-09-25 11:26:02 +0000
commit7aa90628acdd67f2fb1f986ed916855707eb49bf (patch)
treeada94e85f1cd1b8ea0397473433c0dd447d5b4ea /addons/l10n_fr
parent429263af7dd58120aaf1ed46ce48beaab95d417f (diff)
[REM] l10n_fr_sale_closing: This module is squashed into l10n_fr.
Task ID: 2039160
Diffstat (limited to 'addons/l10n_fr')
-rw-r--r--addons/l10n_fr/data/account_sale_closure_cron.xml34
-rw-r--r--addons/l10n_fr/models/account_closing.py159
-rw-r--r--addons/l10n_fr/models/res_company.py64
-rw-r--r--addons/l10n_fr/security/account_closing_intercompany.xml8
-rw-r--r--addons/l10n_fr/security/ir.model.access.csv2
-rw-r--r--addons/l10n_fr/views/account_sale_closure.xml67
6 files changed, 334 insertions, 0 deletions
diff --git a/addons/l10n_fr/data/account_sale_closure_cron.xml b/addons/l10n_fr/data/account_sale_closure_cron.xml
new file mode 100644
index 00000000000..ba2bd2f3a4b
--- /dev/null
+++ b/addons/l10n_fr/data/account_sale_closure_cron.xml
@@ -0,0 +1,34 @@
+<odoo>
+ <record model="ir.cron" id="account_sale_closing_daily">
+ <field name="name">Generate Daily Sales Closing</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">days</field>
+ <field name="numbercall">-1</field>
+ <field name="doall" eval="False"/>
+ <field name="model_id" ref="model_account_sale_closing"/>
+ <field name="state">code</field>
+ <field name="code">model._automated_closing('daily')</field>
+ </record>
+
+ <record model="ir.cron" id="account_sale_closing_monthly">
+ <field name="name">Generate Monthly Sales Closing</field>
+ <field name="interval_number">1</field>
+ <field name="interval_type">months</field>
+ <field name="numbercall">-1</field>
+ <field name="doall" eval="False"/>
+ <field name="model_id" ref="model_account_sale_closing"/>
+ <field name="state">code</field>
+ <field name="code">model._automated_closing('monthly')</field>
+ </record>
+
+ <record model="ir.cron" id="account_sale_closing_annually">
+ <field name="name">Generate Annual Sales Closing</field>
+ <field name="interval_number">12</field>
+ <field name="interval_type">months</field>
+ <field name="numbercall">-1</field>
+ <field name="doall" eval="False"/>
+ <field name="model_id" ref="model_account_sale_closing"/>
+ <field name="state">code</field>
+ <field name="code">model._automated_closing('annually')</field>
+ </record>
+</odoo>
diff --git a/addons/l10n_fr/models/account_closing.py b/addons/l10n_fr/models/account_closing.py
new file mode 100644
index 00000000000..c498c978ea7
--- /dev/null
+++ b/addons/l10n_fr/models/account_closing.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, timedelta
+
+from odoo import models, api, fields
+from odoo.fields import Datetime as FieldDateTime
+from odoo.tools.translate import _
+from odoo.exceptions import UserError
+
+
+class AccountClosing(models.Model):
+ """
+ This object holds an interval total and a grand total of the accounts of type receivable for a company,
+ as well as the last account_move that has been counted in a previous object
+ It takes its earliest brother to infer from when the computation needs to be done
+ in order to compute its own data.
+ """
+ _name = 'account.sale.closing'
+ _order = 'date_closing_stop desc, sequence_number desc'
+ _description = "Sale Closing"
+
+ name = fields.Char(help="Frequency and unique sequence number", required=True)
+ company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True)
+ date_closing_stop = fields.Datetime(string="Closing Date", help='Date to which the values are computed', readonly=True, required=True)
+ date_closing_start = fields.Datetime(string="Starting Date", help='Date from which the total interval is computed', readonly=True, required=True)
+ frequency = fields.Selection(string='Closing Type', selection=[('daily', 'Daily'), ('monthly', 'Monthly'), ('annually', 'Annual')], readonly=True, required=True)
+ total_interval = fields.Monetary(string="Period Total", help='Total in receivable accounts during the interval, excluding overlapping periods', readonly=True, required=True)
+ cumulative_total = fields.Monetary(string="Cumulative Grand Total", help='Total in receivable accounts since the beginnig of times', readonly=True, required=True)
+ sequence_number = fields.Integer('Sequence #', readonly=True, required=True)
+ last_move_id = fields.Many2one('account.move', string='Last journal entry', help='Last Journal entry included in the grand total', readonly=True)
+ last_move_hash = fields.Char(string='Last journal entry\'s inalteralbility hash', readonly=True)
+ currency_id = fields.Many2one('res.currency', string='Currency', help="The company's currency", readonly=True, related='company_id.currency_id', store=True)
+
+ def _query_for_aml(self, company, first_move_sequence_number, date_start):
+ params = {'company_id': company.id}
+ query = '''WITH aggregate AS (SELECT m.id AS move_id,
+ aml.balance AS balance,
+ aml.id as line_id
+ FROM account_move_line aml
+ JOIN account_journal j ON aml.journal_id = j.id
+ JOIN account_account acc ON acc.id = aml.account_id
+ JOIN account_account_type t ON (t.id = acc.user_type_id AND t.type = 'receivable')
+ JOIN account_move m ON m.id = aml.move_id
+ WHERE j.type = 'sale'
+ AND aml.company_id = %(company_id)s
+ AND m.state = 'posted' '''
+
+ if first_move_sequence_number is not False and first_move_sequence_number is not None:
+ params['first_move_sequence_number'] = first_move_sequence_number
+ query += '''AND m.secure_sequence_number > %(first_move_sequence_number)s'''
+ elif date_start:
+ #the first time we compute the closing, we consider only from the installation of the module
+ params['date_start'] = date_start
+ query += '''AND m.date >= %(date_start)s'''
+
+ query += " ORDER BY m.secure_sequence_number DESC) "
+ query += '''SELECT array_agg(move_id) AS move_ids,
+ array_agg(line_id) AS line_ids,
+ sum(balance) AS balance
+ FROM aggregate'''
+
+ self.env.cr.execute(query, params)
+ return self.env.cr.dictfetchall()[0]
+
+ def _compute_amounts(self, frequency, company):
+ """
+ Method used to compute all the business data of the new object.
+ It will search for previous closings of the same frequency to infer the move from which
+ account move lines should be fetched.
+ @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
+ frequencies are literal (daily means 24 hours and so on)
+ @param {recordset} company: the company for which the closing is done
+ @return {dict} containing {field: value} for each business field of the object
+ """
+ interval_dates = self._interval_dates(frequency, company)
+ previous_closing = self.search([
+ ('frequency', '=', frequency),
+ ('company_id', '=', company.id)], limit=1, order='sequence_number desc')
+
+ first_move = self.env['account.move']
+ date_start = interval_dates['interval_from']
+ cumulative_total = 0
+ if previous_closing:
+ first_move = previous_closing.last_move_id
+ date_start = previous_closing.create_date
+ cumulative_total += previous_closing.cumulative_total
+
+ aml_aggregate = self._query_for_aml(company, first_move.secure_sequence_number, date_start)
+
+ total_interval = aml_aggregate['balance'] or 0
+ cumulative_total += total_interval
+
+ # We keep the reference to avoid gaps (like daily object during the weekend)
+ last_move = first_move
+ if aml_aggregate['move_ids']:
+ last_move = last_move.browse(aml_aggregate['move_ids'][0])
+
+ return {'total_interval': total_interval,
+ 'cumulative_total': cumulative_total,
+ 'last_move_id': last_move.id,
+ 'last_move_hash': last_move.inalterable_hash,
+ 'date_closing_stop': interval_dates['date_stop'],
+ 'date_closing_start': date_start,
+ 'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10]}
+
+ def _interval_dates(self, frequency, company):
+ """
+ Method used to compute the theoretical date from which account move lines should be fetched
+ @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
+ frequencies are literal (daily means 24 hours and so on)
+ @param {recordset} company: the company for which the closing is done
+ @return {dict} the theoretical date from which account move lines are fetched.
+ date_stop date to which the move lines are fetched, always now()
+ the dates are in their Odoo Database string representation
+ """
+ date_stop = datetime.utcnow()
+ interval_from = None
+ name_interval = ''
+ if frequency == 'daily':
+ interval_from = date_stop - timedelta(days=1)
+ name_interval = _('Daily Closing')
+ elif frequency == 'monthly':
+ month_target = date_stop.month > 1 and date_stop.month - 1 or 12
+ year_target = month_target < 12 and date_stop.year or date_stop.year - 1
+ interval_from = date_stop.replace(year=year_target, month=month_target)
+ name_interval = _('Monthly Closing')
+ elif frequency == 'annually':
+ year_target = date_stop.year - 1
+ interval_from = date_stop.replace(year=year_target)
+ name_interval = _('Annual Closing')
+
+ return {'interval_from': FieldDateTime.to_string(interval_from),
+ 'date_stop': FieldDateTime.to_string(date_stop),
+ 'name_interval': name_interval}
+
+ def write(self, vals):
+ raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
+
+ def unlink(self):
+ raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
+
+ @api.model
+ def _automated_closing(self, frequency='daily'):
+ """To be executed by the CRON to create an object of the given frequency for each company that needs it
+ @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
+ frequencies are literal (daily means 24 hours and so on)
+ @return {recordset} all the objects created for the given frequency
+ """
+ res_company = self.env['res.company'].search([])
+ account_closings = self.env['account.sale.closing']
+ for company in res_company.filtered(lambda c: c._is_accounting_unalterable()):
+ new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id()
+ values = self._compute_amounts(frequency, company)
+ values['frequency'] = frequency
+ values['company_id'] = company.id
+ values['sequence_number'] = new_sequence_number
+ account_closings |= account_closings.create(values)
+
+ return account_closings
diff --git a/addons/l10n_fr/models/res_company.py b/addons/l10n_fr/models/res_company.py
new file mode 100644
index 00000000000..4088a1afac4
--- /dev/null
+++ b/addons/l10n_fr/models/res_company.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, api, _
+
+
+class ResCompany(models.Model):
+ _inherit = 'res.company'
+
+ l10n_fr_closing_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to build sale closings', readonly=True)
+ siret = fields.Char(related='partner_id.siret', string='SIRET', size=14, readonly=False)
+ ape = fields.Char(string='APE')
+
+ @api.model
+ def _get_unalterable_country(self):
+ return ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF'] # These codes correspond to France and DOM-TOM.
+
+ def _is_vat_french(self):
+ return self.vat and self.vat.startswith('FR') and len(self.vat) == 13
+
+ def _is_accounting_unalterable(self):
+ if not self.vat and not self.country_id:
+ return False
+ return self.country_id and self.country_id.code in self._get_unalterable_country()
+
+ @api.model
+ def create(self, vals):
+ company = super(ResCompany, self).create(vals)
+ #when creating a new french company, create the securisation sequence as well
+ if company._is_accounting_unalterable():
+ sequence_fields = ['l10n_fr_closing_sequence_id']
+ company._create_secure_sequence(sequence_fields)
+ return company
+
+ def write(self, vals):
+ res = super(ResCompany, self).write(vals)
+ #if country changed to fr, create the securisation sequence
+ for company in self:
+ if company._is_accounting_unalterable():
+ sequence_fields = ['l10n_fr_closing_sequence_id']
+ company._create_secure_sequence(sequence_fields)
+ return res
+
+ def _create_secure_sequence(self, sequence_fields):
+ """This function creates a no_gap sequence on each company in self that will ensure
+ a unique number is given to all posted account.move in such a way that we can always
+ find the previous move of a journal entry on a specific journal.
+ """
+ for company in self:
+ vals_write = {}
+ for seq_field in sequence_fields:
+ if not company[seq_field]:
+ vals = {
+ 'name': _('Securisation of %s - %s') % (seq_field, company.name),
+ 'code': 'FRSECURE%s-%s' % (company.id, seq_field),
+ 'implementation': 'no_gap',
+ 'prefix': '',
+ 'suffix': '',
+ 'padding': 0,
+ 'company_id': company.id}
+ seq = self.env['ir.sequence'].create(vals)
+ vals_write[seq_field] = seq.id
+ if vals_write:
+ company.write(vals_write)
diff --git a/addons/l10n_fr/security/account_closing_intercompany.xml b/addons/l10n_fr/security/account_closing_intercompany.xml
new file mode 100644
index 00000000000..a316014794e
--- /dev/null
+++ b/addons/l10n_fr/security/account_closing_intercompany.xml
@@ -0,0 +1,8 @@
+<odoo>
+ <record model="ir.rule" id="account_sale_closing_multi_company">
+ <field name="name">Sale Closing multi-company</field>
+ <field name="model_id" ref="model_account_sale_closing"/>
+ <field name="domain_force">[('company_id', 'in', company_ids)]</field>
+ </record>
+
+</odoo>
diff --git a/addons/l10n_fr/security/ir.model.access.csv b/addons/l10n_fr/security/ir.model.access.csv
new file mode 100644
index 00000000000..a3c5001986d
--- /dev/null
+++ b/addons/l10n_fr/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_l10n_fr_account_sale_closing_user,l10n_fr.account.sale.closing.user,l10n_fr.model_account_sale_closing,base.group_user,1,0,0,0 \ No newline at end of file
diff --git a/addons/l10n_fr/views/account_sale_closure.xml b/addons/l10n_fr/views/account_sale_closure.xml
new file mode 100644
index 00000000000..e33549ffb4a
--- /dev/null
+++ b/addons/l10n_fr/views/account_sale_closure.xml
@@ -0,0 +1,67 @@
+<odoo>
+ <record id="list_view_account_sale_closing" model="ir.ui.view">
+ <field name="name">Sales Closings</field>
+ <field name="model">account.sale.closing</field>
+ <field name="arch" type="xml">
+ <tree create="false" import="false">
+ <field name="date_closing_start"/>
+ <field name="date_closing_stop"/>
+ <field name="company_id" groups="base.group_multi_company"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="frequency"/>
+ <field name="sequence_number" groups="base.group_no_one"/>
+ <field name="total_interval"/>
+ <field name="cumulative_total"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="form_view_account_sale_closing" model="ir.ui.view">
+ <field name="name">Sales Closings</field>
+ <field name="model">account.sale.closing</field>
+ <field name="arch" type="xml">
+ <form create="false" edit="false" string="Account Closing">
+ <sheet>
+ <div class="oe_title">
+ <h1>
+ <field name="name"/>
+ </h1>
+ </div>
+ <group>
+ <group>
+ <field name="date_closing_start"/>
+ <field name="date_closing_stop"/>
+ <field name="frequency"/>
+ <field name="sequence_number" groups="base.group_no_one"/>
+ </group>
+ <group>
+ <field name="total_interval"/>
+ <field name="cumulative_total"/>
+ <field name="last_move_id" groups="account.group_account_user"/>
+ <field name="last_move_hash" groups="account.group_account_user"/>
+ </group>
+ <group>
+ <field name="company_id" groups="base.group_multi_company"/>
+ <field name="currency_id" invisible="1"/>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_list_view_account_sale_closing" model="ir.actions.act_window">
+ <field name="name">Sales Closings</field>
+ <field name="res_model">account.sale.closing</field>
+ <field name="view_mode">list,form</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_nocreate">
+ The closings are created by Odoo
+ </p><p>
+ Sales closings run automatically on a daily, monthly and annual basis. It computes both period and cumulative totals from all the sales entries posted in the system after the previous closing.
+ </p>
+ </field>
+ </record>
+
+ <menuitem action="action_list_view_account_sale_closing" id="menu_account_closing_reporting" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
+</odoo>