From d0e4982574716113c5081b8dbd09a4cdee30bc1d Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Wed, 25 Oct 2023 15:26:33 +0200 Subject: [PATCH] [ADD] account_cutoff_accrual_sale --- account_cutoff_accrual_sale/README.rst | 0 account_cutoff_accrual_sale/__init__.py | 4 + account_cutoff_accrual_sale/__manifest__.py | 20 +++++ account_cutoff_accrual_sale/data/ir_cron.xml | 20 +++++ .../models/__init__.py | 7 ++ .../models/account_cutoff.py | 12 +++ .../models/account_cutoff_line.py | 26 ++++++ .../models/account_move.py | 15 ++++ .../models/sale_order_line.py | 80 +++++++++++++++++++ .../readme/CONFIGURE.rst | 7 ++ .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 25 ++++++ .../views/account_cutoff.xml | 31 +++++++ .../views/account_cutoff_line.xml | 22 +++++ .../odoo/addons/account_cutoff_accrual_sale | 1 + setup/account_cutoff_accrual_sale/setup.py | 6 ++ 16 files changed, 279 insertions(+) create mode 100644 account_cutoff_accrual_sale/README.rst create mode 100644 account_cutoff_accrual_sale/__init__.py create mode 100644 account_cutoff_accrual_sale/__manifest__.py create mode 100644 account_cutoff_accrual_sale/data/ir_cron.xml create mode 100644 account_cutoff_accrual_sale/models/__init__.py create mode 100644 account_cutoff_accrual_sale/models/account_cutoff.py create mode 100644 account_cutoff_accrual_sale/models/account_cutoff_line.py create mode 100644 account_cutoff_accrual_sale/models/account_move.py create mode 100644 account_cutoff_accrual_sale/models/sale_order_line.py create mode 100644 account_cutoff_accrual_sale/readme/CONFIGURE.rst create mode 100644 account_cutoff_accrual_sale/readme/CONTRIBUTORS.rst create mode 100644 account_cutoff_accrual_sale/readme/DESCRIPTION.rst create mode 100644 account_cutoff_accrual_sale/views/account_cutoff.xml create mode 100644 account_cutoff_accrual_sale/views/account_cutoff_line.xml create mode 120000 setup/account_cutoff_accrual_sale/odoo/addons/account_cutoff_accrual_sale create mode 100644 setup/account_cutoff_accrual_sale/setup.py diff --git a/account_cutoff_accrual_sale/README.rst b/account_cutoff_accrual_sale/README.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/account_cutoff_accrual_sale/__init__.py b/account_cutoff_accrual_sale/__init__.py new file mode 100644 index 00000000000..fc446a6607e --- /dev/null +++ b/account_cutoff_accrual_sale/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import models diff --git a/account_cutoff_accrual_sale/__manifest__.py b/account_cutoff_accrual_sale/__manifest__.py new file mode 100644 index 00000000000..b30faa653e4 --- /dev/null +++ b/account_cutoff_accrual_sale/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Account Cut-off Accrual Sale", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "license": "AGPL-3", + "summary": "Accrued Revenue on Sales Order", + "author": "BCIM, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-closing", + "depends": ["account_cutoff_accrual_order_base", "sale"], + "data": [ + "views/account_cutoff.xml", + "views/account_cutoff_line.xml", + "data/ir_cron.xml", + ], + "installable": True, + "application": True, +} diff --git a/account_cutoff_accrual_sale/data/ir_cron.xml b/account_cutoff_accrual_sale/data/ir_cron.xml new file mode 100644 index 00000000000..e041a379be2 --- /dev/null +++ b/account_cutoff_accrual_sale/data/ir_cron.xml @@ -0,0 +1,20 @@ + + + + + Make cutoff at end of period - sales order lines + + + code + model._cron_cutoff("accrued_revenue", "sale.order.line") + + 1 + months + -1 + + + + + diff --git a/account_cutoff_accrual_sale/models/__init__.py b/account_cutoff_accrual_sale/models/__init__.py new file mode 100644 index 00000000000..043553ff7f2 --- /dev/null +++ b/account_cutoff_accrual_sale/models/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import account_cutoff +from . import account_cutoff_line +from . import sale_order_line +from . import account_move diff --git a/account_cutoff_accrual_sale/models/account_cutoff.py b/account_cutoff_accrual_sale/models/account_cutoff.py new file mode 100644 index 00000000000..73517104fc9 --- /dev/null +++ b/account_cutoff_accrual_sale/models/account_cutoff.py @@ -0,0 +1,12 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class AccountCutoff(models.Model): + _inherit = "account.cutoff" + + order_line_model = fields.Selection( + selection_add=[("sale.order.line", "Sales Orders")] + ) diff --git a/account_cutoff_accrual_sale/models/account_cutoff_line.py b/account_cutoff_accrual_sale/models/account_cutoff_line.py new file mode 100644 index 00000000000..b58868546ba --- /dev/null +++ b/account_cutoff_accrual_sale/models/account_cutoff_line.py @@ -0,0 +1,26 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, fields, models + + +class AccountCutoffLine(models.Model): + _inherit = "account.cutoff.line" + + sale_line_id = fields.Many2one( + comodel_name="sale.order.line", string="Sales Order Line", readonly=True + ) + sale_order_id = fields.Many2one(related="sale_line_id.order_id") + + def _get_order_line(self): + if self.sale_line_id: + return self.sale_line_id + return super()._get_order_line() + + @api.depends("sale_line_id") + def _compute_invoice_lines(self): + for rec in self: + if rec.sale_line_id: + rec.invoice_line_ids = rec.sale_line_id.invoice_lines + super()._compute_invoice_lines() + return diff --git a/account_cutoff_accrual_sale/models/account_move.py b/account_cutoff_accrual_sale/models/account_move.py new file mode 100644 index 00000000000..db04bfe664e --- /dev/null +++ b/account_cutoff_accrual_sale/models/account_move.py @@ -0,0 +1,15 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _get_cutoff_accrual_order_lines(self): + """Return a list of order lines to process""" + res = super()._get_cutoff_accrual_order_lines() + if self.move_type in ("out_invoice", "out_refund"): + res.append(self.invoice_line_ids.sale_line_ids) + return res diff --git a/account_cutoff_accrual_sale/models/sale_order_line.py b/account_cutoff_accrual_sale/models/sale_order_line.py new file mode 100644 index 00000000000..3110fd7cc7e --- /dev/null +++ b/account_cutoff_accrual_sale/models/sale_order_line.py @@ -0,0 +1,80 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM sprl) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class SaleOrderLine(models.Model): + _name = "sale.order.line" + _inherit = ["sale.order.line", "order.line.cutoff.accrual.mixin"] + + account_cutoff_line_ids = fields.One2many( + "account.cutoff.line", + "sale_line_id", + string="Account Cutoff Lines", + readonly=True, + ) + + def _get_cutoff_accrual_partner(self): + return self.order_id.partner_invoice_id + + def _get_cutoff_accrual_product_qty(self): + return self.product_uom_qty + + def _prepare_cutoff_accrual_line(self, cutoff): + res = super()._prepare_cutoff_accrual_line(cutoff) + if not res: + return + res["sale_line_id"] = self.id + return res + + def _get_cutoff_accrual_lines_invoiced_after(self, cutoff): + cutoff_nextday = cutoff._nextday_start_dt() + # Take all invoices impacting the cutoff + # FIXME: what about ("move_id.payment_state", "=", "invoicing_legacy") + domain = [ + ("move_id.move_type", "in", ("out_invoice", "out_refund")), + ("sale_line_ids", "!=", False), + "|", + ("move_id.state", "=", "draft"), + "&", + ("move_id.state", "=", "posted"), + ("move_id.date", ">=", cutoff_nextday), + ] + if self.env.company.cutoff_exclude_locked_orders: + domain += [("sale_line_ids.order_id.state", "!=", "done")] + invoice_line_after = self.env["account.move.line"].search(domain, order="id") + _logger.debug( + "Sales Invoice Lines done after cutoff: %s" % len(invoice_line_after) + ) + sale_ids = set(invoice_line_after.sale_line_ids.order_id.ids) + sales = self.env["sale.order"].browse(sale_ids) + return sales.order_line + + def _get_cutoff_accrual_lines_delivered_after(self, cutoff): + # FIXME sale_stock + cutoff_nextday = cutoff._nextday_start_dt() + # Take all moves done after the cutoff date + moves_after = self.env["stock.move"].search( + [ + ("state", "=", "done"), + ("date", ">=", cutoff_nextday), + ("sale_line_id", "!=", False), + ], + order="id", + ) + sale_ids = set(moves_after.sale_line_id.order_id.ids) + sales = self.env["sale.order"].browse(sale_ids) + return sales.order_line + + def _get_cutoff_accrual_delivered_quantity(self, cutoff): + self.ensure_one() + return self.qty_delivered + + @api.model + def _cron_cutoff_accrual(self): + self._cron_cutoff("accrued_revenue", self._model) diff --git a/account_cutoff_accrual_sale/readme/CONFIGURE.rst b/account_cutoff_accrual_sale/readme/CONFIGURE.rst new file mode 100644 index 00000000000..b7ad4bec8a9 --- /dev/null +++ b/account_cutoff_accrual_sale/readme/CONFIGURE.rst @@ -0,0 +1,7 @@ +To configure this module, you need to: + +#. Go to the accounting settings to select the journals and accounts used for + the cutoff. +#. Analytic accounting needs to be enable in Accounting - Settings. +#. If you want to also accrue the taxes, you need in Accounting - Taxes, for + each type of taxes an accrued tax account. diff --git a/account_cutoff_accrual_sale/readme/CONTRIBUTORS.rst b/account_cutoff_accrual_sale/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..fbb1899fef6 --- /dev/null +++ b/account_cutoff_accrual_sale/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Alexis de Lattre (Akretion) +* Jacques-Etienne Baudoux (BCIM) +* Thierry Ducrest diff --git a/account_cutoff_accrual_sale/readme/DESCRIPTION.rst b/account_cutoff_accrual_sale/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..868ce9595ad --- /dev/null +++ b/account_cutoff_accrual_sale/readme/DESCRIPTION.rst @@ -0,0 +1,25 @@ +This module extends the functionality of account_cutoff_accrual_order_base +to allow the computation of revenue cutoffs on sales orders. + +The accrual is computed by comparing on the order, the quantity +delivered/received and the quantity invoiced. In case, some deliveries or +invoices have occurred after the cutoff date, those quantities can be affected +and are recomputed. This allows to quickly generate a cutoff snapshot by +processing few lines. + +For SO, you can make the difference between: +* invoice to generate (delivered qty > invoiced qty) +* goods to send (prepayment) (delivered qty < invoiced qty) + +At each end of period, a cron job generates the cutoff entries for the revenues +(based on SO). + +You can configure to disable the generation of cutoff entries for closed orders. + +Once the cutoff lines have been generated but the accounting entries are not yet +created, you are still able to create or modify invoices before the accounting +butoff date. The cutoff lines will be adapted automatically to reflect the new +situation. + +Once the cutoff accounting entries are generated you cannot create or modify +invoices before the accounting cutoff date. diff --git a/account_cutoff_accrual_sale/views/account_cutoff.xml b/account_cutoff_accrual_sale/views/account_cutoff.xml new file mode 100644 index 00000000000..3dbd47f948d --- /dev/null +++ b/account_cutoff_accrual_sale/views/account_cutoff.xml @@ -0,0 +1,31 @@ + + + + + + Accrued Expense on Sales Orders + account.cutoff + tree,form + [('order_line_model', '=', 'sale.order.line')] + {'default_order_line_model': 'sale.order.line', 'default_cutoff_type': 'accrued_revenue'} + +

+ Click to start preparing a new revenue accrual. +

+ This view can be used by accountants in order to collect information about accrued expenses. It then allows to generate the corresponding cut-off journal entry in one click. +

+
+
+ + +
diff --git a/account_cutoff_accrual_sale/views/account_cutoff_line.xml b/account_cutoff_accrual_sale/views/account_cutoff_line.xml new file mode 100644 index 00000000000..8947386a3f0 --- /dev/null +++ b/account_cutoff_accrual_sale/views/account_cutoff_line.xml @@ -0,0 +1,22 @@ + + + + + + account.cutoff.line + + + + + + + + + + diff --git a/setup/account_cutoff_accrual_sale/odoo/addons/account_cutoff_accrual_sale b/setup/account_cutoff_accrual_sale/odoo/addons/account_cutoff_accrual_sale new file mode 120000 index 00000000000..67c826a6e64 --- /dev/null +++ b/setup/account_cutoff_accrual_sale/odoo/addons/account_cutoff_accrual_sale @@ -0,0 +1 @@ +../../../../account_cutoff_accrual_sale \ No newline at end of file diff --git a/setup/account_cutoff_accrual_sale/setup.py b/setup/account_cutoff_accrual_sale/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_accrual_sale/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)