From 9434d2892611bd3559eff91f4eee3f24b0ade323 Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Wed, 25 Oct 2023 13:48:58 +0200 Subject: [PATCH 1/7] [IMP] account_cutoff_base: _prepare_moves Add hook on counter-part moves generation --- account_cutoff_base/models/account_cutoff.py | 63 +++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/account_cutoff_base/models/account_cutoff.py b/account_cutoff_base/models/account_cutoff.py index 4118e5d448d..d9df1c2ebed 100644 --- a/account_cutoff_base/models/account_cutoff.py +++ b/account_cutoff_base/models/account_cutoff.py @@ -13,6 +13,19 @@ from odoo.tools.misc import format_date +class PosNeg: + def __init__(self, amount): + self.amount_pos = self.amount_neg = 0 + self.__iadd__(amount) + + def __iadd__(self, amount): + if amount < 0: + self.amount_neg += amount + else: + self.amount_pos += amount + return self + + class AccountCutoff(models.Model): _name = "account.cutoff" _rec_name = "cutoff_date" @@ -208,11 +221,18 @@ def _get_merge_keys(self): def _prepare_move(self, to_provision): self.ensure_one() movelines_to_create = [] - amount_total = 0 + amount_total_pos = 0 + amount_total_neg = 0 ref = self.move_ref + company_currency = self.company_id.currency_id + cur_rprec = company_currency.rounding merge_keys = self._get_merge_keys() for merge_values, amount in to_provision.items(): - amount = self.company_currency_id.round(amount) + amount_total_neg += self.company_currency_id.round(amount.amount_neg) + amount_total_pos += self.company_currency_id.round(amount.amount_pos) + amount = amount.amount_pos + amount.amount_neg + if float_is_zero(amount, precision_rounding=cur_rprec): + continue vals = { "debit": amount < 0 and amount * -1 or 0, "credit": amount >= 0 and amount or 0, @@ -226,20 +246,10 @@ def _prepare_move(self, to_provision): vals[k] = value movelines_to_create.append((0, 0, vals)) - amount_total += amount # add counter-part - counterpart_amount = self.company_currency_id.round(amount_total * -1) - movelines_to_create.append( - ( - 0, - 0, - { - "account_id": self.cutoff_account_id.id, - "debit": counterpart_amount < 0 and counterpart_amount * -1 or 0, - "credit": counterpart_amount >= 0 and counterpart_amount or 0, - }, - ) + movelines_to_create += self._prepare_counterpart_moves( + to_provision, amount_total_pos, amount_total_neg ) res = { @@ -251,6 +261,26 @@ def _prepare_move(self, to_provision): } return res + def _prepare_counterpart_moves( + self, to_provision, amount_total_pos, amount_total_neg + ): + amount = (amount_total_pos + amount_total_neg) * -1 + company_currency = self.company_id.currency_id + cur_rprec = company_currency.rounding + if float_is_zero(amount, precision_rounding=cur_rprec): + return [] + return [ + ( + 0, + 0, + { + "account_id": self.cutoff_account_id.id, + "debit": amount < 0 and amount * -1 or 0, + "credit": amount >= 0 and amount or 0, + }, + ) + ] + def _prepare_provision_line(self, cutoff_line): """Convert a cutoff line to elements of a move line. @@ -295,7 +325,10 @@ def _merge_provision_lines(self, provision_lines): or provision_line.get(key) for key in merge_keys ) - to_provision[key] += provision_line["amount"] + if key in to_provision: + to_provision[key] += provision_line["amount"] + else: + to_provision[key] = PosNeg(provision_line["amount"]) return to_provision def create_move(self): From d3c0f3e3643b623f7ebf98806a76c8bf6337df07 Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 10 Oct 2024 17:44:59 +0200 Subject: [PATCH 2/7] [ADD] account_cutoff_accrual_order_base --- .github/workflows/test.yml | 16 +- account_cutoff_accrual_order_base/README.rst | 121 +++++ account_cutoff_accrual_order_base/__init__.py | 4 + .../__manifest__.py | 21 + account_cutoff_accrual_order_base/i18n/fr.po | 175 +++++++ .../models/__init__.py | 7 + .../models/account_cutoff.py | 108 +++++ .../models/account_cutoff_line.py | 69 +++ .../models/account_move.py | 37 ++ .../models/order_line_mixin.py | 273 +++++++++++ .../readme/CONFIGURE.rst | 7 + .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 24 + .../static/description/index.html | 456 ++++++++++++++++++ .../tests/__init__.py | 0 .../tests/common.py | 84 ++++ .../views/account_cutoff_line_view.xml | 75 +++ .../views/account_cutoff_view.xml | 23 + .../addons/account_cutoff_accrual_order_base | 1 + .../setup.py | 6 + 20 files changed, 1508 insertions(+), 2 deletions(-) create mode 100644 account_cutoff_accrual_order_base/README.rst create mode 100644 account_cutoff_accrual_order_base/__init__.py create mode 100644 account_cutoff_accrual_order_base/__manifest__.py create mode 100644 account_cutoff_accrual_order_base/i18n/fr.po create mode 100644 account_cutoff_accrual_order_base/models/__init__.py create mode 100644 account_cutoff_accrual_order_base/models/account_cutoff.py create mode 100644 account_cutoff_accrual_order_base/models/account_cutoff_line.py create mode 100644 account_cutoff_accrual_order_base/models/account_move.py create mode 100644 account_cutoff_accrual_order_base/models/order_line_mixin.py create mode 100644 account_cutoff_accrual_order_base/readme/CONFIGURE.rst create mode 100644 account_cutoff_accrual_order_base/readme/CONTRIBUTORS.rst create mode 100644 account_cutoff_accrual_order_base/readme/DESCRIPTION.rst create mode 100644 account_cutoff_accrual_order_base/static/description/index.html create mode 100644 account_cutoff_accrual_order_base/tests/__init__.py create mode 100644 account_cutoff_accrual_order_base/tests/common.py create mode 100644 account_cutoff_accrual_order_base/views/account_cutoff_line_view.xml create mode 100644 account_cutoff_accrual_order_base/views/account_cutoff_view.xml create mode 120000 setup/account_cutoff_accrual_order_base/odoo/addons/account_cutoff_accrual_order_base create mode 100644 setup/account_cutoff_accrual_order_base/setup.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1af7438faf..ed6455529ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,22 @@ jobs: matrix: include: - container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest - name: test with Odoo + makepot: "true" + exclude: "account_cutoff_picking" + name: test with Odoo w/o account_cutoff_picking - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest - name: test with OCB + exclude: "account_cutoff_picking" + name: test with OCB w/o account_cutoff_picking + - container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest makepot: "true" + include: "account_cutoff_base,purchase_stock,sale_stock,account_cutoff_picking" + name: test with Odoo w/ account_cutoff_picking + - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest + include: "account_cutoff_base,purchase_stock,sale_stock,account_cutoff_picking" + name: test with OCB w/ account_cutoff_picking + env: + INCLUDE: "${{ matrix.include }}" + EXCLUDE: "${{ matrix.exclude }}" services: postgres: image: postgres:12.0 diff --git a/account_cutoff_accrual_order_base/README.rst b/account_cutoff_accrual_order_base/README.rst new file mode 100644 index 00000000000..b31c1289e41 --- /dev/null +++ b/account_cutoff_accrual_order_base/README.rst @@ -0,0 +1,121 @@ +================================== +Account Cut-off Accrual Order Base +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4dc4b5f6f8cd4c3ee94cce6a5168a31e624341c7c059ffbf60e32af36e7083c7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--closing-lightgray.png?logo=github + :target: https://github.com/OCA/account-closing/tree/16.0/account_cutoff_accrual_order_base + :alt: OCA/account-closing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-closing-16-0/account-closing-16-0-account_cutoff_accrual_order_base + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-closing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of account_cutoff_base +to allow the computation of expense and revenue cutoffs on 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. + +You can configure to disable the generation of cutoff entries on orders. +For instance, if you know you will never receive the missing invoiced goods, +you can disable cutoff entries on a purchase order. + +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. +Nevertheless, you can still reset to draft a supplier invoice but you won't be +able to modify any amount. You are then supposed to re-validate the invoice. + +Warning: This module is replacing account_cutoff_picking and is incompatible with it. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +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. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Alexis de Lattre (Akretion) +* Jacques-Etienne Baudoux (BCIM) +* Thierry Ducrest + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/account-closing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_cutoff_accrual_order_base/__init__.py b/account_cutoff_accrual_order_base/__init__.py new file mode 100644 index 00000000000..86337901b11 --- /dev/null +++ b/account_cutoff_accrual_order_base/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/account_cutoff_accrual_order_base/__manifest__.py b/account_cutoff_accrual_order_base/__manifest__.py new file mode 100644 index 00000000000..a2cd03a9711 --- /dev/null +++ b/account_cutoff_accrual_order_base/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +{ + "name": "Account Cut-off Accrual Order Base", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "license": "AGPL-3", + "summary": "Accrued Order Base", + "author": "BCIM, Akretion, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/account-closing", + "depends": ["account_cutoff_base"], + "data": [ + "views/account_cutoff_view.xml", + "views/account_cutoff_line_view.xml", + ], + "installable": True, + "application": False, +} diff --git a/account_cutoff_accrual_order_base/i18n/fr.po b/account_cutoff_accrual_order_base/i18n/fr.po new file mode 100644 index 00000000000..44760e25cde --- /dev/null +++ b/account_cutoff_accrual_order_base/i18n/fr.po @@ -0,0 +1,175 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_cutoff_accrual_order_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 11:53+0000\n" +"PO-Revision-Date: 2023-10-26 11:53+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_cutoff_accrual_order_base +#: model_terms:ir.ui.view,arch_db:account_cutoff_accrual_order_base.account_cutoff_form +msgid " on " +msgstr " sur " + +#. module: account_cutoff_accrual_order_base +#: model:ir.model,name:account_cutoff_accrual_order_base.model_account_cutoff +msgid "Account Cut-off" +msgstr "Provision" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model,name:account_cutoff_accrual_order_base.model_account_cutoff_line +msgid "Account Cut-off Line" +msgstr "Ligne de provision" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__amount +msgid "Amount" +msgstr "Montant" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,help:account_cutoff_accrual_order_base.field_account_cutoff_line__amount +msgid "Amount that is used as base to compute the Cut-off Amount." +msgstr "Montant qui est utilisé comme base pour calculer le montant de la provision" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model,name:account_cutoff_accrual_order_base.model_res_company +msgid "Companies" +msgstr "Sociétés" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model,name:account_cutoff_accrual_order_base.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de configuration" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__cutoff_amount +msgid "Cut-off Amount" +msgstr "Montant de la provision" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,help:account_cutoff_accrual_order_base.field_account_cutoff_line__cutoff_amount +msgid "Cut-off Amount without taxes in the Company Currency." +msgstr "Montant de provision hors taxes dans la devise de la société." + +#. module: account_cutoff_accrual_order_base +#: model:ir.model,name:account_cutoff_accrual_order_base.model_order_line_cutoff_accrual_mixin +msgid "Cutoff Accrual Order Line Mixin" +msgstr "" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_res_company__cutoff_exclude_locked_orders +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_res_config_settings__cutoff_exclude_locked_orders +msgid "Cutoff Exclude Locked Orders" +msgstr "Provision Exclure les commandes verrouillées" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,help:account_cutoff_accrual_order_base.field_res_company__cutoff_exclude_locked_orders +#: model:ir.model.fields,help:account_cutoff_accrual_order_base.field_res_config_settings__cutoff_exclude_locked_orders +msgid "Do not generate cut-off entries for orders that are locked" +msgstr "Ne pas générer d'écriture de provision pour les commande verrouillées" + +#. module: account_cutoff_accrual_order_base +#. odoo-python +#: code:addons/account_cutoff_accrual_order_base/models/order_line_mixin.py:0 +#, python-format +msgid "Error: Missing '%(label)s' on tax '%(name)s'." +msgstr "Errur: '%(label)s' manquant sur la taxe '%(name)s'." + +#. module: account_cutoff_accrual_order_base +#. odoo-python +#: code:addons/account_cutoff_accrual_order_base/models/account_cutoff.py:0 +#, python-format +msgid "" +"Error: Missing {map_type} account on product '{product}' or on related " +"product category." +msgstr "" +"Erreur: {map_type} compte manquant sur le produit '{product}' ou sur " +"la catégorie de product liée." + +#. module: account_cutoff_accrual_order_base +#: model_terms:ir.ui.view,arch_db:account_cutoff_accrual_order_base.res_config_settings_view_form +msgid "Exclude Locked Orders" +msgstr "Exclure les commandes verrouillées" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__invoice_line_ids +#: model_terms:ir.ui.view,arch_db:account_cutoff_accrual_order_base.account_cutoff_line_form +msgid "Invoice Lines" +msgstr "Lignes de facture" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__invoiced_qty +msgid "Invoiced Quantity" +msgstr "Quantité facturée" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model,name:account_cutoff_accrual_order_base.model_account_move +msgid "Journal Entry" +msgstr "Pièce comptable" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff__order_line_model +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__order_line_model +msgid "Order Line Model" +msgstr "" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__product_id +msgid "Product" +msgstr "Produit" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__quantity +msgid "Quantity" +msgstr "Quantité" + +#. module: account_cutoff_accrual_order_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_base.field_account_cutoff_line__received_qty +msgid "Received Quantity" +msgstr "Quantité reçue" + +#. module: account_cutoff_accrual_order_base +#. odoo-python +#: code:addons/account_cutoff_accrual_order_base/models/order_line_mixin.py:0 +#, python-format +msgid "Wrong cutoff type %s" +msgstr "Mauvais type de cutoff %s" + +#. module: account_cutoff_accrual_order_base +#. odoo-python +#: code:addons/account_cutoff_accrual_order_base/models/account_move.py:0 +#, python-format +msgid "" +"You cannot validate an invoice for an accounting date that generates an entry in a closed cut-off (i.e. for which an accounting entry has already been created).\n" +" - Cut-off: {cutoff}\n" +" - Product: {product}\n" +msgstr "" +"Vous ne pouvez pas valider une facture à une date comptable qui génère une ligne dans une provision fermée (càd pour laquelle l'écriture comptable de provision a déjà été créée).\n" +" - Provision: {cutoff}\n" +" - Produit: {product}\n" + +#. module: account_cutoff_accrual_order_base +#. odoo-python +#: code:addons/account_cutoff_accrual_order_base/models/account_move.py:0 +#, python-format +msgid "" +"You cannot validate an invoice for an accounting date that modifies a closed cutoff (i.e. for which an accounting entry has already been created).\n" +" - Cut-off: {cutoff}\n" +" - Product: {product}\n" +" - Previous invoiced quantity: {prev_inv_qty}\n" +" - New invoiced quantity: {new_inv_qty}" +msgstr "" +"Vous ne pouvez pas valider une facture à une date comptable qui modifie une provision fermée (càd pour laquelle l'écriture comptable de provision a déjà été créée).\n" +" - Provision: {cutoff}\n" +" - Produit: {product}\n" +" - Précédente quantité facturée: {prev_inv_qty}\n" +" - Nouvelle quantité facturée: {new_inv_qty}" diff --git a/account_cutoff_accrual_order_base/models/__init__.py b/account_cutoff_accrual_order_base/models/__init__.py new file mode 100644 index 00000000000..324c49690b0 --- /dev/null +++ b/account_cutoff_accrual_order_base/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.html). + +from . import account_cutoff +from . import account_cutoff_line +from . import order_line_mixin +from . import account_move diff --git a/account_cutoff_accrual_order_base/models/account_cutoff.py b/account_cutoff_accrual_order_base/models/account_cutoff.py new file mode 100644 index 00000000000..184141cdecc --- /dev/null +++ b/account_cutoff_accrual_order_base/models/account_cutoff.py @@ -0,0 +1,108 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging +from datetime import datetime, time, timedelta + +import pytz +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools import split_every + +_logger = logging.getLogger(__name__) + + +class AccountCutoff(models.Model): + _inherit = "account.cutoff" + + order_line_model = fields.Selection( + selection=[], + readonly=True, + ) + + def _nextday_start_dt(self): + """Convert the cutoff date into datetime as start of next day.""" + next_day = self.cutoff_date + timedelta(days=1) + tz = self.env.company.partner_id.tz or "UTC" + start_next_day = datetime.combine( + next_day, time(0, 0, 0, 0, tzinfo=pytz.timezone(tz)) + ) + return start_next_day.replace(tzinfo=None) + + def _get_product_account(self, product, fpos): + if self.cutoff_type in "accrued_revenue": + map_type = "income" + elif self.cutoff_type in "accrued_expense": + map_type = "expense" + else: + return + account = product.product_tmpl_id.get_product_accounts(fpos)[map_type] + if not account: + raise UserError( + _( + "Error: Missing {map_type} account on product '{product}' or on" + " related product category.", + ).format( + map_type=map_type, + product=product.name, + ) + ) + return account + + def get_lines(self): + self.ensure_one() + # If the computation of the cutoff is done at the cutoff date, then we + # only need to retrieve lines where there is a qty to invoice (i.e. + # delivered qty != invoiced qty). + # For any line where a move or an invoice has been done after the + # cutoff date, we need to recompute the quantities. + res = super().get_lines() + if not self.order_line_model: + return res + + model = self.env[self.order_line_model] + _logger.debug("Get model lines") + line_ids = set(model.browse(model._get_cutoff_accrual_lines_query(self)).ids) + _logger.debug("Get model lines invoiced after") + line_ids |= set(model._get_cutoff_accrual_lines_invoiced_after(self).ids) + _logger.debug("Get model lines delivered after") + line_ids |= set(model._get_cutoff_accrual_lines_delivered_after(self).ids) + + _logger.debug("Prepare cutoff lines per chunks") + # A good chunk size is per 1000. If bigger, it is not faster but memory + # usage increases. If too low, then it takes more cpu time. + for chunk in split_every(models.INSERT_BATCH_SIZE * 10, tuple(line_ids)): + lines = model.browse(chunk) + values = [] + for line in lines: + data = line._prepare_cutoff_accrual_line(self) + if not data: + continue + values.append(data) + self.env["account.cutoff.line"].create(values) + # free memory usage + self.env.invalidate_all() + _logger.debug("Prepare cutoff lines - next chunk") + return res + + @api.model + def _cron_cutoff(self, cutoff_type, model): + # Cron is expected to run at begin of new period. We need the last day + # of previous month. Support some time difference and compute last day + # of previous period. + last_day = datetime.today() + if last_day.day > 20: + last_day += relativedelta(months=1) + last_day = last_day.replace(day=1) + last_day -= relativedelta(days=1) + cutoff = self.with_context(default_cutoff_type=cutoff_type).create( + { + "cutoff_date": last_day, + "cutoff_type": cutoff_type, + "order_line_model": model, + "auto_reverse": True, + } + ) + cutoff.get_lines() diff --git a/account_cutoff_accrual_order_base/models/account_cutoff_line.py b/account_cutoff_accrual_order_base/models/account_cutoff_line.py new file mode 100644 index 00000000000..2542dd785d5 --- /dev/null +++ b/account_cutoff_accrual_order_base/models/account_cutoff_line.py @@ -0,0 +1,69 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# Copyright 2013 Alexis de Lattre (Akretion) +# 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" + + order_line_model = fields.Selection(related="parent_id.order_line_model") + + product_id = fields.Many2one( + comodel_name="product.product", string="Product", readonly=True + ) + received_qty = fields.Float("Received Quantity", readonly=True) + invoiced_qty = fields.Float("Invoiced Quantity", readonly=True) + invoice_line_ids = fields.One2many( + "account.move.line", + compute="_compute_invoice_lines", + string="Invoice Lines", + ) + quantity = fields.Float(compute="_compute_quantity", store=True) + amount = fields.Monetary(compute="_compute_amount", store=True) + cutoff_amount = fields.Monetary(compute="_compute_cutoff_amount", store=True) + + def _get_order_line(self): + self.ensure_one() + return + + def _compute_invoice_lines(self): + return + + @api.depends("invoiced_qty", "received_qty") + def _compute_quantity(self): + for rec in self: + if not rec.parent_id.order_line_model: + continue + rec.quantity = rec.received_qty - rec.invoiced_qty + + @api.depends("price_unit", "quantity") + def _compute_amount(self): + for rec in self: + if not rec.parent_id.order_line_model: + continue + if rec.parent_id.cutoff_type == "accrued_revenue": + amount = rec.quantity * rec.price_unit + elif rec.parent_id.cutoff_type == "accrued_expense": + amount = -rec.quantity * rec.price_unit + else: + continue + rec.amount = rec.company_currency_id.round(amount) + + @api.depends("amount") + def _compute_cutoff_amount(self): + for rec in self: + if not rec.parent_id.order_line_model: + continue + if rec.parent_id.state == "done": + continue + if rec.company_currency_id != rec.currency_id: + currency_at_date = rec.currency_id.with_context( + date=rec.parent_id.cutoff_date + ) + rec.cutoff_amount = currency_at_date.compute( + rec.amount, rec.company_currency_id + ) + else: + rec.cutoff_amount = rec.amount diff --git a/account_cutoff_accrual_order_base/models/account_move.py b/account_cutoff_accrual_order_base/models/account_move.py new file mode 100644 index 00000000000..cdc06d2aeeb --- /dev/null +++ b/account_cutoff_accrual_order_base/models/account_move.py @@ -0,0 +1,37 @@ +# 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 _post(self, soft=True): + res = super()._post(soft=soft) + self._update_cutoff_accrual_order() + return res + + def unlink(self): + # In case the invoice was posted, we need to check any affected cutoff + self._update_cutoff_accrual_order() + return super().unlink() + + def _get_cutoff_accrual_order_lines(self): + """Return a list of order lines to process""" + self.ensure_one() + return [] + + def _update_cutoff_accrual_order(self): + for move in self: + if not move.is_invoice(): + continue + for model_order_lines in move.sudo()._get_cutoff_accrual_order_lines(): + for order_line in model_order_lines: + # In case invoice lines have been created and posted in one + # transaction, we need to clear the cache of invoice lines + # on the cutoff lines + order_line.account_cutoff_line_ids.invalidate_recordset( + ["invoice_line_ids"] + ) + order_line._update_cutoff_accrual(move.date) diff --git a/account_cutoff_accrual_order_base/models/order_line_mixin.py b/account_cutoff_accrual_order_base/models/order_line_mixin.py new file mode 100644 index 00000000000..7d8ccd65abd --- /dev/null +++ b/account_cutoff_accrual_order_base/models/order_line_mixin.py @@ -0,0 +1,273 @@ +# Copyright 2013 Alexis de Lattre (Akretion) +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +from odoo import Command, _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class OrderLineCutoffAccrualMixin(models.AbstractModel): + _name = "order.line.cutoff.accrual.mixin" + _description = "Cutoff Accrual Order Line Mixin" + + is_cutoff_accrual_excluded = fields.Boolean( + string="Do not generate cut-off entries", + readonly=True, + inverse=lambda r: r._inverse_is_cutoff_accrual_excluded(), + ) + + def _inverse_is_cutoff_accrual_excluded(self): + for rec in self: + rec.sudo()._update_cutoff_accrual() + + def _get_cutoff_accrual_partner(self): + self.ensure_one() + return self.order_id.partner_id + + def _get_cutoff_accrual_fiscal_position(self): + self.ensure_one() + return self.order_id.fiscal_position_id + + def _get_cutoff_accrual_product(self): + self.ensure_one() + return self.product_id + + def _get_cutoff_accrual_product_qty(self): + return self.product_qty + + def _get_cutoff_accrual_price_unit(self): + self.ensure_one() + product_qty = self._get_cutoff_accrual_product_qty() + if product_qty: + return self.price_subtotal / product_qty + return 0 + + def _get_cutoff_accrual_invoice_lines(self): + self.ensure_one() + return self.invoice_lines + + def _get_cutoff_accrual_invoiced_quantity(self, cutoff): + self.ensure_one() + cutoff_nextday = cutoff._nextday_start_dt() + invoiced_qty = sum( + line.quantity + * (-1 if line.move_id.move_type in ("in_refund", "out_refund") else 1) + for line in self._get_cutoff_accrual_invoice_lines() + if ( + line.move_id.state == "posted" + and line.move_id.date <= cutoff.cutoff_date + ) + or ( + line.move_id.state == "draft" + and line.move_id.move_type == "in_refund" + and line.move_id.create_date < cutoff_nextday + ) + ) + return invoiced_qty + + def _get_cutoff_accrual_lines_invoiced_after(self, cutoff): + """Return order lines""" + return NotImplemented() + + def _get_cutoff_accrual_line_delivered_after(self, cutoff): + """Return order lines""" + return self.browse() + + def _get_cutoff_accrual_delivered_service_quantity(self, cutoff): + return NotImplemented() + + def _get_cutoff_accrual_delivered_stock_quantity(self, cutoff): + return NotImplemented() + + def _get_cutoff_accrual_delivered_quantity(self, cutoff): + self.ensure_one() + if self.product_id.detailed_type == "service": + return self._get_cutoff_accrual_delivered_service_quantity(cutoff) + return self._get_cutoff_accrual_delivered_stock_quantity(cutoff) + + def _get_cutoff_accrual_lines_delivered_after(self, cutoff): + return self.browse() + + def _get_cutoff_accrual_delivered_min_date(self): + """Return first delivery date""" + return False + + def _get_cutoff_accrual_taxes(self, cutoff, quantity): + self.ensure_one() + if cutoff.cutoff_type == "accrued_revenue": + tax_account_field_name = "account_accrued_revenue_id" + tax_account_field_label = "Accrued Revenue Tax Account" + sign = 1 + elif cutoff.cutoff_type == "accrued_expense": + tax_account_field_name = "account_accrued_expense_id" + tax_account_field_label = "Accrued Expense Tax Account" + sign = -1 + else: + return + tax_line_ids = [Command.clear()] + base_line = self._convert_to_tax_base_line_dict() + base_line["quantity"] = quantity + tax_info = self.env["account.tax"]._compute_taxes([base_line]) + for tax_line in tax_info["tax_lines_to_add"]: + amount = tax_line["tax_amount"] * sign + if cutoff.company_currency_id != self.currency_id: + currency_at_date = self.currency_id.with_context( + date=self.parent_id.cutoff_date + ) + tax_cutoff_amount = currency_at_date.compute( + amount, cutoff.company_currency_id + ) + else: + tax_cutoff_amount = amount + tax = self.env["account.tax"].browse(tax_line["tax_id"]) + tax_cutoff_account_id = tax[tax_account_field_name] + if not tax_cutoff_account_id: + raise UserError( + _( + "Error: Missing '%(label)s' on tax '%(name)s'.", + label=tax_account_field_label, + name=tax.display_name, + ) + ) + tax_line_ids.append( + Command.create( + { + "tax_id": tax_line["tax_id"], + "base": tax_line["base_amount"], + "amount": amount, + "cutoff_account_id": tax_cutoff_account_id.id, + "cutoff_amount": tax_cutoff_amount, + }, + ) + ) + return tax_line_ids + + @api.model + def _get_cutoff_accrual_lines_domain(self, cutoff): + domain = [] + domain.append(("is_cutoff_accrual_excluded", "!=", True)) + return domain + + @api.model + def _get_cutoff_accrual_lines_query(self, cutoff): + domain = self._get_cutoff_accrual_lines_domain(cutoff) + self._flush_search(domain) + query = self._where_calc(domain) + self._apply_ir_rules(query, "read") + return query + + def _prepare_cutoff_accrual_line(self, cutoff): + """ + Calculate accrual using order line + """ + self.ensure_one() + if cutoff.cutoff_type not in ("accrued_expense", "accrued_revenue"): + return UserError(_("Wrong cutoff type %s") % cutoff.cutoff_type) + price_unit = self._get_cutoff_accrual_price_unit() + if not price_unit: + return {} + fpos = self._get_cutoff_accrual_fiscal_position() + account = cutoff._get_product_account(self.product_id, fpos) + cutoff_account_id = cutoff._get_mapping_dict().get(account.id, account.id) + res = { + "parent_id": cutoff.id, + "partner_id": self._get_cutoff_accrual_partner().id, + "name": self.name, + "account_id": account.id, + "cutoff_account_id": cutoff_account_id, + "analytic_distribution": self.analytic_distribution, + "currency_id": self.currency_id.id, + "product_id": self._get_cutoff_accrual_product().id, + "price_unit": price_unit, + } + delivered_qty = self._get_cutoff_accrual_delivered_quantity(cutoff) + invoiced_qty = self._get_cutoff_accrual_invoiced_quantity(cutoff) + if delivered_qty == invoiced_qty: + return {} + res["received_qty"] = delivered_qty + res["invoiced_qty"] = invoiced_qty + quantity = delivered_qty - invoiced_qty + if self.env.company.accrual_taxes: + res["tax_line_ids"] = self._get_cutoff_accrual_taxes(cutoff, quantity) + return res + + def _update_cutoff_accrual(self, date=False): + self.ensure_one() + if self.is_cutoff_accrual_excluded: + self.account_cutoff_line_ids.filtered( + lambda line: line.parent_id.state != "done" + ).unlink() + return + for cutoff_line in self.account_cutoff_line_ids: + cutoff = cutoff_line.parent_id + invoiced_qty = ( + cutoff_line._get_order_line()._get_cutoff_accrual_invoiced_quantity( + cutoff + ) + ) + if cutoff.state == "done" and invoiced_qty != cutoff_line.invoiced_qty: + raise UserError( + _( + "You cannot validate an invoice for an accounting date " + "that modifies a closed cutoff (i.e. for which an " + "accounting entry has already been created).\n" + " - Cut-off: {cutoff}\n" + " - Product: {product}\n" + " - Previous invoiced quantity: {prev_inv_qty}\n" + " - New invoiced quantity: {new_inv_qty}" + ).format( + cutoff=cutoff.display_name, + product=cutoff_line.product_id.display_name, + prev_inv_qty=cutoff_line.invoiced_qty, + new_inv_qty=invoiced_qty, + ) + ) + cutoff_line.invoiced_qty = invoiced_qty + # search missing cutoff entries - start at first reception + domain = [ + ( + "id", + "not in", + self.account_cutoff_line_ids.parent_id.ids, + ), + ("cutoff_type", "in", ("accrued_expense", "accrued_revenue")), + ("order_line_model", "=", self._name), + ("company_id", "=", self.company_id.id), + ] + if date: + # When invoice is updated + delivery_min_date = self._get_cutoff_accrual_delivered_min_date() + if delivery_min_date: + date = min(delivery_min_date, date) + else: + date = date + domain.append(("cutoff_date", ">=", date)) + else: + # When is_cutoff_accrual_excluded is removed + domain.append(("state", "!=", "done")) + cutoffs = self.env["account.cutoff"].sudo().search(domain) + values = [] + for cutoff in cutoffs: + data = self._prepare_cutoff_accrual_line(cutoff) + if not data: + continue + if cutoff.state == "done": + raise UserError( + _( + "You cannot validate an invoice for an accounting date " + "that generates an entry in a closed cut-off (i.e. for " + "which an accounting entry has already been created).\n" + " - Cut-off: {cutoff}\n" + " - Product: {product}\n" + ).format( + cutoff=cutoff.display_name, + product=self.product_id.display_name, + ) + ) + values.append(data) + if values: + self.env["account.cutoff.line"].sudo().create(values) diff --git a/account_cutoff_accrual_order_base/readme/CONFIGURE.rst b/account_cutoff_accrual_order_base/readme/CONFIGURE.rst new file mode 100644 index 00000000000..b7ad4bec8a9 --- /dev/null +++ b/account_cutoff_accrual_order_base/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_order_base/readme/CONTRIBUTORS.rst b/account_cutoff_accrual_order_base/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..fbb1899fef6 --- /dev/null +++ b/account_cutoff_accrual_order_base/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Alexis de Lattre (Akretion) +* Jacques-Etienne Baudoux (BCIM) +* Thierry Ducrest diff --git a/account_cutoff_accrual_order_base/readme/DESCRIPTION.rst b/account_cutoff_accrual_order_base/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..b0189d30a2a --- /dev/null +++ b/account_cutoff_accrual_order_base/readme/DESCRIPTION.rst @@ -0,0 +1,24 @@ +This module extends the functionality of account_cutoff_base +to allow the computation of expense and revenue cutoffs on 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. + +You can configure to disable the generation of cutoff entries on orders. +For instance, if you know you will never receive the missing invoiced goods, +you can disable cutoff entries on a purchase order. + +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. +Nevertheless, you can still reset to draft a supplier invoice but you won't be +able to modify any amount. You are then supposed to re-validate the invoice. + +Warning: This module is replacing account_cutoff_picking and is incompatible with it. diff --git a/account_cutoff_accrual_order_base/static/description/index.html b/account_cutoff_accrual_order_base/static/description/index.html new file mode 100644 index 00000000000..defc5bf215f --- /dev/null +++ b/account_cutoff_accrual_order_base/static/description/index.html @@ -0,0 +1,456 @@ + + + + + + +Account Cut-off Accrual Order Base + + + +
+

Account Cut-off Accrual Order Base

+ + +

Beta License: AGPL-3 OCA/account-closing Translate me on Weblate Try me on Runboat

+

This module extends the functionality of account_cutoff_base +to allow the computation of expense and revenue cutoffs on 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.

+

You can configure to disable the generation of cutoff entries on orders. +For instance, if you know you will never receive the missing invoiced goods, +you can disable cutoff entries on a purchase order.

+

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. +Nevertheless, you can still reset to draft a supplier invoice but you won’t be +able to modify any amount. You are then supposed to re-validate the invoice.

+

Warning: This module is replacing account_cutoff_picking and is incompatible with it.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to the accounting settings to select the journals and accounts used for +the cutoff.
  2. +
  3. Analytic accounting needs to be enable in Accounting - Settings.
  4. +
  5. If you want to also accrue the taxes, you need in Accounting - Taxes, for +each type of taxes an accrued tax account.
  6. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/account-closing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_cutoff_accrual_order_base/tests/__init__.py b/account_cutoff_accrual_order_base/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/account_cutoff_accrual_order_base/tests/common.py b/account_cutoff_accrual_order_base/tests/common.py new file mode 100644 index 00000000000..564603ea564 --- /dev/null +++ b/account_cutoff_accrual_order_base/tests/common.py @@ -0,0 +1,84 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase + + +class TestAccountCutoffAccrualOrderCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.company = cls.env.ref("base.main_company") + cls.cutoff_journal = cls.env["account.journal"].create( + { + "code": "cop0", + "company_id": cls.company.id, + "name": "Cutoff Journal Picking", + "type": "general", + } + ) + cls.cutoff_account = cls.env["account.account"].create( + { + "name": "Cutoff account", + "code": "ACC480000", + "company_id": cls.company.id, + "account_type": "liability_current", + } + ) + cls.company.write( + { + "default_accrued_revenue_account_id": cls.cutoff_account.id, + "default_accrued_expense_account_id": cls.cutoff_account.id, + "default_cutoff_journal_id": cls.cutoff_journal.id, + } + ) + + cls.partner = cls.env.ref("base.res_partner_1") + cls.products = cls.env.ref("product.product_delivery_01") | cls.env.ref( + "product.product_delivery_02" + ) + cls.stock_location = cls.env.ref("stock.stock_location_stock") + for p in cls.products: + cls.env["stock.quant"]._update_available_quantity( + p, cls.stock_location, 100 + ) + cls.products |= cls.env.ref("product.expense_product") + # analytic account + cls.default_plan = cls.env["account.analytic.plan"].create( + {"name": "Default", "company_id": False} + ) + cls.analytic_account = cls.env["account.analytic.account"].create( + { + "name": "analytic_account", + "plan_id": cls.default_plan.id, + "company_id": False, + } + ) + + def _refund_invoice(self, invoice, post=True): + credit_note_wizard = ( + self.env["account.move.reversal"] + .with_context( + **{ + "active_ids": invoice.ids, + "active_id": invoice.id, + "active_model": "account.move", + "tz": self.env.company.partner_id.tz or "UTC", + } + ) + .create( + { + "refund_method": "refund", + "reason": "refund", + "journal_id": invoice.journal_id.id, + } + ) + ) + invoice_refund = self.env["account.move"].browse( + credit_note_wizard.reverse_moves()["res_id"] + ) + invoice_refund.ref = invoice_refund.id + if post: + invoice_refund.action_post() + return invoice_refund diff --git a/account_cutoff_accrual_order_base/views/account_cutoff_line_view.xml b/account_cutoff_accrual_order_base/views/account_cutoff_line_view.xml new file mode 100644 index 00000000000..9fde85c651d --- /dev/null +++ b/account_cutoff_accrual_order_base/views/account_cutoff_line_view.xml @@ -0,0 +1,75 @@ + + + + + + + account.cutoff.line + + + + + + + + + + + + + + + + + + + + + + + + + + + + account.cutoff.line + + + + + + + + + + + + + + diff --git a/account_cutoff_accrual_order_base/views/account_cutoff_view.xml b/account_cutoff_accrual_order_base/views/account_cutoff_view.xml new file mode 100644 index 00000000000..b1fd066e3c1 --- /dev/null +++ b/account_cutoff_accrual_order_base/views/account_cutoff_view.xml @@ -0,0 +1,23 @@ + + + + + + account.cutoff + + + + on + + + {'invisible': [('order_line_model', '!=', False)]} + + + + + diff --git a/setup/account_cutoff_accrual_order_base/odoo/addons/account_cutoff_accrual_order_base b/setup/account_cutoff_accrual_order_base/odoo/addons/account_cutoff_accrual_order_base new file mode 120000 index 00000000000..96d16d0bd9c --- /dev/null +++ b/setup/account_cutoff_accrual_order_base/odoo/addons/account_cutoff_accrual_order_base @@ -0,0 +1 @@ +../../../../account_cutoff_accrual_order_base \ No newline at end of file diff --git a/setup/account_cutoff_accrual_order_base/setup.py b/setup/account_cutoff_accrual_order_base/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_accrual_order_base/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 4d8ef43c1df115ea9387d6d26a0c25909f1a5bce Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 10 Oct 2024 17:45:30 +0200 Subject: [PATCH 3/7] [ADD] account_cutoff_accrual_sale --- account_cutoff_accrual_sale/README.rst | 119 +++++ account_cutoff_accrual_sale/__init__.py | 5 + account_cutoff_accrual_sale/__manifest__.py | 22 + account_cutoff_accrual_sale/data/ir_cron.xml | 20 + account_cutoff_accrual_sale/hooks.py | 13 + account_cutoff_accrual_sale/i18n/fr.po | 82 ++++ .../models/__init__.py | 8 + .../models/account_cutoff.py | 12 + .../models/account_cutoff_line.py | 26 + .../models/account_move.py | 15 + .../models/sale_order.py | 18 + .../models/sale_order_line.py | 111 +++++ .../readme/CONFIGURE.rst | 7 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 25 + .../static/description/index.html | 453 ++++++++++++++++++ account_cutoff_accrual_sale/tests/__init__.py | 1 + account_cutoff_accrual_sale/tests/common.py | 69 +++ .../tests/test_cutoff_revenue.py | 327 +++++++++++++ .../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 + 23 files changed, 1394 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/hooks.py create mode 100644 account_cutoff_accrual_sale/i18n/fr.po 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.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/static/description/index.html create mode 100644 account_cutoff_accrual_sale/tests/__init__.py create mode 100644 account_cutoff_accrual_sale/tests/common.py create mode 100644 account_cutoff_accrual_sale/tests/test_cutoff_revenue.py 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..4e4ab76b038 --- /dev/null +++ b/account_cutoff_accrual_sale/README.rst @@ -0,0 +1,119 @@ +============================ +Account Cut-off Accrual Sale +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e6ea6da9fe3bf4d5eb4f9d59a1b019ab7587e921d11c65ba85ea7ee414df1897 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--closing-lightgray.png?logo=github + :target: https://github.com/OCA/account-closing/tree/16.0/account_cutoff_accrual_sale + :alt: OCA/account-closing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-closing-16-0/account-closing-16-0-account_cutoff_accrual_sale + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-closing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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). + +Orders forced in status invoiced won't have cutoff entries. + +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. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +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. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/account-closing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_cutoff_accrual_sale/__init__.py b/account_cutoff_accrual_sale/__init__.py new file mode 100644 index 00000000000..b525a90d0e7 --- /dev/null +++ b/account_cutoff_accrual_sale/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import models +from .hooks import post_init_hook diff --git a/account_cutoff_accrual_sale/__manifest__.py b/account_cutoff_accrual_sale/__manifest__.py new file mode 100644 index 00000000000..4b8349e4eee --- /dev/null +++ b/account_cutoff_accrual_sale/__manifest__.py @@ -0,0 +1,22 @@ +# 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)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/account-closing", + "depends": ["account_cutoff_accrual_order_base", "sale", "sale_force_invoiced"], + "data": [ + "views/account_cutoff.xml", + "views/account_cutoff_line.xml", + "data/ir_cron.xml", + ], + "post_init_hook": "post_init_hook", + "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/hooks.py b/account_cutoff_accrual_sale/hooks.py new file mode 100644 index 00000000000..bede18ffe05 --- /dev/null +++ b/account_cutoff_accrual_sale/hooks.py @@ -0,0 +1,13 @@ +# Copyright 2023 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +def post_init_hook(cr, registry): + cr.execute( + """ + UPDATE sale_order_line + SET is_cutoff_accrual_excluded = TRUE + WHERE order_id IN + ( SELECT id FROM sale_order WHERE force_invoiced ) + """ + ) diff --git a/account_cutoff_accrual_sale/i18n/fr.po b/account_cutoff_accrual_sale/i18n/fr.po new file mode 100644 index 00000000000..492ce48a95f --- /dev/null +++ b/account_cutoff_accrual_sale/i18n/fr.po @@ -0,0 +1,82 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_cutoff_accrual_sale +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 11:51+0000\n" +"PO-Revision-Date: 2023-10-26 11:51+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_cutoff_accrual_sale +#: model:ir.model,name:account_cutoff_accrual_sale.model_account_cutoff +msgid "Account Cut-off" +msgstr "Provision" + +#. module: account_cutoff_accrual_sale +#: model:ir.model,name:account_cutoff_accrual_sale.model_account_cutoff_line +msgid "Account Cut-off Line" +msgstr "Ligne de provision" + +#. module: account_cutoff_accrual_sale +#: model:ir.model.fields,field_description:account_cutoff_accrual_sale.field_sale_order_line__account_cutoff_line_ids +msgid "Account Cutoff Lines" +msgstr "Lignes de provision" + +#. module: account_cutoff_accrual_sale +#: model:ir.actions.act_window,name:account_cutoff_accrual_sale.account_accrual_action +#: model:ir.ui.menu,name:account_cutoff_accrual_sale.account_accrual_menu +msgid "Accrued Revenue on Sales Orders" +msgstr "Provisions sur Ventes" + +#. module: account_cutoff_accrual_sale +#: model_terms:ir.actions.act_window,help:account_cutoff_accrual_sale.account_accrual_action +msgid "Click to start preparing a new revenue accrual." +msgstr "" + +#. module: account_cutoff_accrual_sale +#: model:ir.model,name:account_cutoff_accrual_sale.model_account_move +msgid "Journal Entry" +msgstr "Pièce comptable" + +#. module: account_cutoff_accrual_sale +#: model:ir.actions.server,name:account_cutoff_accrual_sale.ir_cron_cutoff_revenue_ir_actions_server +#: model:ir.cron,cron_name:account_cutoff_accrual_sale.ir_cron_cutoff_revenue +msgid "Make cutoff at end of period - sales order lines" +msgstr "" + +#. module: account_cutoff_accrual_sale +#: model:ir.model.fields,field_description:account_cutoff_accrual_sale.field_account_cutoff__order_line_model +msgid "Order Line Model" +msgstr "" + +#. module: account_cutoff_accrual_sale +#: model:ir.model.fields,field_description:account_cutoff_accrual_sale.field_account_cutoff_line__sale_order_id +msgid "Order Reference" +msgstr "Commande" + +#. module: account_cutoff_accrual_sale +#: model:ir.model,name:account_cutoff_accrual_sale.model_sale_order_line +#: model:ir.model.fields,field_description:account_cutoff_accrual_sale.field_account_cutoff_line__sale_line_id +msgid "Sales Order Line" +msgstr "Ligne de commande" + +#. module: account_cutoff_accrual_sale +#: model:ir.model.fields.selection,name:account_cutoff_accrual_sale.selection__account_cutoff__order_line_model__sale_order_line +msgid "Sales Orders" +msgstr "Ventes" + +#. module: account_cutoff_accrual_sale +#: model_terms:ir.actions.act_window,help:account_cutoff_accrual_sale.account_accrual_action +msgid "" +"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." +msgstr "" diff --git a/account_cutoff_accrual_sale/models/__init__.py b/account_cutoff_accrual_sale/models/__init__.py new file mode 100644 index 00000000000..ba6147ff2e5 --- /dev/null +++ b/account_cutoff_accrual_sale/models/__init__.py @@ -0,0 +1,8 @@ +# 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 +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.py b/account_cutoff_accrual_sale/models/sale_order.py new file mode 100644 index 00000000000..af07cdc453f --- /dev/null +++ b/account_cutoff_accrual_sale/models/sale_order.py @@ -0,0 +1,18 @@ +# Copyright 2023 Jacques-Etienne Baudoux (BCIM sprl) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def write(self, vals): + res = super().write(vals) + if "force_invoiced" in vals or vals.get("invoice_status") == "to invoice": + # As the order could be non invoiceable while a line is invoiceable + # (see delivery module), we need to check each line when the order + # invoice status becomes "to invoice" + for line in self.order_line: + line.sudo()._update_cutoff_accrual() + 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..132e6a99023 --- /dev/null +++ b/account_cutoff_accrual_sale/models/sale_order_line.py @@ -0,0 +1,111 @@ +# 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, + ) + + is_cutoff_accrual_excluded = fields.Boolean( + compute="_compute_is_cutoff_accrual_excluded", + store=True, + ) + + @api.depends("order_id.force_invoiced") + def _compute_is_cutoff_accrual_excluded(self): + for rec in self: + # If the order is not to invoice + rec.is_cutoff_accrual_excluded = rec.order_id.force_invoiced + + 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 _get_cutoff_accrual_lines_domain(self, cutoff): + domain = super()._get_cutoff_accrual_lines_domain(cutoff) + # The line could be invoiceable but not the order (see delivery + # module). + domain.append(("invoice_status", "=", "to invoice")) + domain.append(("order_id.invoice_status", "=", "to invoice")) + return domain + + 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 = [ + ("sale_line_ids.is_cutoff_accrual_excluded", "!=", True), + ("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), + ] + 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) + ) + if not invoice_line_after: + return self.env["sale.order.line"] + # In SQL to reduce memory usage as we could process large dataset + self.env.cr.execute( + """ + SELECT order_id + FROM sale_order_line + WHERE id in ( + SELECT order_line_id + FROM sale_order_line_invoice_rel + WHERE invoice_line_id in %s + ) + """, + (tuple(invoice_line_after.ids),), + ) + sale_ids = [x[0] for x in self.env.cr.fetchall()] + lines = self.env["sale.order.line"].search( + [("order_id", "in", sale_ids)], order="id" + ) + return lines + + def _get_cutoff_accrual_delivered_service_quantity(self, cutoff): + self.ensure_one() + cutoff_nextday = cutoff._nextday_start_dt() + if self.create_date >= cutoff_nextday: + # A line added after the cutoff cannot be delivered in the past + return 0 + if self.product_id.invoice_policy == "order": + return self.product_uom_qty + return self.qty_delivered + + def _get_cutoff_accrual_delivered_stock_quantity(self, cutoff): + self.ensure_one() + cutoff_nextday = cutoff._nextday_start_dt() + if self.create_date >= cutoff_nextday: + # A line added after the cutoff cannot be delivered in the past + return 0 + if self.product_id.invoice_policy == "order": + return self.product_uom_qty + return self.qty_delivered 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..3c6c5c696a8 --- /dev/null +++ b/account_cutoff_accrual_sale/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/account_cutoff_accrual_sale/readme/DESCRIPTION.rst b/account_cutoff_accrual_sale/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..5edeb5906a0 --- /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). + +Orders forced in status invoiced won't have cutoff entries. + +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/static/description/index.html b/account_cutoff_accrual_sale/static/description/index.html new file mode 100644 index 00000000000..c94a06adec6 --- /dev/null +++ b/account_cutoff_accrual_sale/static/description/index.html @@ -0,0 +1,453 @@ + + + + + + +Account Cut-off Accrual Sale + + + +
+

Account Cut-off Accrual Sale

+ + +

Beta License: AGPL-3 OCA/account-closing Translate me on Weblate Try me on Runboat

+

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).

+

Orders forced in status invoiced won’t have cutoff entries.

+

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.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to the accounting settings to select the journals and accounts used for +the cutoff.
  2. +
  3. Analytic accounting needs to be enable in Accounting - Settings.
  4. +
  5. If you want to also accrue the taxes, you need in Accounting - Taxes, for +each type of taxes an accrued tax account.
  6. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/account-closing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_cutoff_accrual_sale/tests/__init__.py b/account_cutoff_accrual_sale/tests/__init__.py new file mode 100644 index 00000000000..34d4dd52a80 --- /dev/null +++ b/account_cutoff_accrual_sale/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cutoff_revenue diff --git a/account_cutoff_accrual_sale/tests/common.py b/account_cutoff_accrual_sale/tests/common.py new file mode 100644 index 00000000000..4ee9c79ff75 --- /dev/null +++ b/account_cutoff_accrual_sale/tests/common.py @@ -0,0 +1,69 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import Command, fields + +from odoo.addons.account_cutoff_accrual_order_base.tests.common import ( + TestAccountCutoffAccrualOrderCommon, +) + + +class TestAccountCutoffAccrualSaleCommon(TestAccountCutoffAccrualOrderCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.tax_sale = cls.env.company.account_sale_tax_id + cls.cutoff_account = cls.env["account.account"].create( + { + "name": "account accrued revenue", + "code": "accountAccruedExpense", + "account_type": "asset_current", + "company_id": cls.env.company.id, + } + ) + cls.tax_sale.account_accrued_revenue_id = cls.cutoff_account + # Removing all existing SO + cls.env.cr.execute("DELETE FROM sale_order;") + # Make service product invoicable on order + cls.env.ref("product.expense_product").invoice_policy = "order" + # Create SO and confirm + cls.price = 100 + cls.qty = 5 + cls.so = cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "partner_invoice_id": cls.partner.id, + "partner_shipping_id": cls.partner.id, + "order_line": [ + Command.create( + { + "name": p.name, + "product_id": p.id, + "product_uom_qty": cls.qty, + "product_uom": p.uom_id.id, + "price_unit": cls.price, + "analytic_distribution": { + str(cls.analytic_account.id): 100.0 + }, + "tax_id": [Command.set(cls.tax_sale.ids)], + }, + ) + for p in cls.products + ], + "pricelist_id": cls.env.ref("product.list0").id, + } + ) + # Create cutoff + type_cutoff = "accrued_revenue" + cls.revenue_cutoff = ( + cls.env["account.cutoff"] + .with_context(default_cutoff_type=type_cutoff) + .create( + { + "cutoff_type": type_cutoff, + "order_line_model": "sale.order.line", + "company_id": 1, + "cutoff_date": fields.Date.today(), + } + ) + ) diff --git a/account_cutoff_accrual_sale/tests/test_cutoff_revenue.py b/account_cutoff_accrual_sale/tests/test_cutoff_revenue.py new file mode 100644 index 00000000000..a98f2442d65 --- /dev/null +++ b/account_cutoff_accrual_sale/tests/test_cutoff_revenue.py @@ -0,0 +1,327 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo import Command + +from .common import TestAccountCutoffAccrualSaleCommon + + +class TestAccountCutoffAccrualSale(TestAccountCutoffAccrualSaleCommon): + def test_accrued_revenue_empty(self): + """Test cutoff when there is no confirmed SO.""" + cutoff = self.revenue_cutoff + cutoff.get_lines() + self.assertEqual( + len(cutoff.line_ids), 0, "There should be no SO line to process" + ) + + def test_revenue_analytic_distribution(self): + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + for line in cutoff.line_ids: + self.assertDictEqual( + line.analytic_distribution, + {str(self.analytic_account.id): self.price}, + "Analytic distribution is not correctly set", + ) + + def test_revenue_tax_line(self): + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff lines should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + len(line.tax_line_ids), 1, "tax lines is not correctly set" + ) + self.assertEqual(line.tax_line_ids.cutoff_account_id, self.cutoff_account) + self.assertEqual(line.tax_line_ids.tax_id, self.tax_sale) + self.assertEqual(line.tax_line_ids.base, amount) + self.assertEqual(line.tax_line_ids.amount, amount * 15 / 100) + self.assertEqual(line.tax_line_ids.cutoff_amount, amount * 15 / 100) + + # Make tests for product with invoice policy on order + + def test_accrued_revenue_on_so_not_invoiced(self): + """Test cutoff based on SO where product_uom_qty > qty_invoiced.""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Make invoice + self.so._create_invoices(final=True) + # - invoice is in draft, no change to cutoff + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Validate invoice + self.so.invoice_ids.action_post() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "SO line cutoff amount incorrect") + # Make a refund - the refund reset the SO lines qty_invoiced + self._refund_invoice(self.so.invoice_ids) + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff lines should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_delivery_not_invoiced(self): + """Test cutoff based on SO where product_uom_qty > qty_invoiced.""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + # 1 cutoff line for the service + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + delivered_qty = ( + cutoff.line_ids.sale_line_id._get_cutoff_accrual_delivered_quantity(cutoff) + ) + self.assertEqual(delivered_qty, 5, "the delivery line should be delivered") + # simulate a delivery service line added after cutoff date + cutoff.cutoff_date -= timedelta(days=1) + delivered_qty = ( + cutoff.line_ids.sale_line_id._get_cutoff_accrual_delivered_quantity(cutoff) + ) + self.assertEqual(delivered_qty, 0, "the delivery line should not be delivered") + # regenerate cutoff + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "0 cutoff line should be found") + + def test_accrued_revenue_on_so_all_invoiced(self): + """Test cutoff based on SO where product_uom_qty = qty_invoiced.""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + # Make invoice + self.so._create_invoices(final=True) + # Validate invoice + self.so.invoice_ids.action_post() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "No cutoff lines should be found") + # Make a refund - the refund reset qty_invoiced + self._refund_invoice(self.so.invoice_ids) + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_draft_invoice(self): + """Test cutoff based on SO where product_uom_qty = qty_invoiced but the. + + invoice is still in draft + """ + cutoff = self.revenue_cutoff + self.so.action_confirm() + # Make invoice + self.so._create_invoices(final=True) + # - invoice is in draft, no change to cutoff + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Validate invoice + self.so.invoice_ids.action_post() + self.assertEqual(len(cutoff.line_ids), 1, "no cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "SO line cutoff amount incorrect") + # Make a refund - the refund reset SO lines qty_invoiced + self._refund_invoice(self.so.invoice_ids) + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff lines should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_not_invoiced_after_cutoff(self): + """Test cutoff based on SO where product_uom_qty > qty_invoiced. + + And make invoice after cutoff date + """ + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + # Make invoice + self.so._create_invoices(final=True) + # - invoice is in draft, no change to cutoff + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Validate invoice after cutoff + self.so.invoice_ids.invoice_date = cutoff.cutoff_date + timedelta(days=1) + self.so.invoice_ids.action_post() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff lines should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Make a refund after cutoff + refund = self._refund_invoice(self.so.invoice_ids, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_all_invoiced_after_cutoff(self): + """Test cutoff based on SO where product_uom_qty = qty_invoiced. + + And make invoice after cutoff date + """ + cutoff = self.revenue_cutoff + self.so.action_confirm() + # Make invoice + self.so._create_invoices(final=True) + # Validate invoice after cutoff + self.so.invoice_ids.invoice_date = cutoff.cutoff_date + timedelta(days=1) + self.so.invoice_ids.action_post() + # as there is no delivery and invoice is after cutoff, no line is generated + self.assertEqual(len(cutoff.line_ids), 0, "No cutoff lines should be found") + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Make a refund - the refund reset SO lines qty_invoiced + refund = self._refund_invoice(self.so.invoice_ids, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_after(self): + """Test cutoff when SO is force invoiced after cutoff""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Force invoiced after cutoff lines generated, lines should be deleted + self.so.force_invoiced = True + self.assertEqual(len(cutoff.line_ids), 0, "cutoff line should be deleted") + # Remove Force invoiced, lines should be recreated + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff lines should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_before(self): + """Test cutoff when SO is force invoiced before cutoff""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + # Force invoiced before cutoff lines generated, lines should be deleted + self.so.force_invoiced = True + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") + # Remove Force invoiced, lines should be created + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_after_but_posted(self): + """Test cutoff when SO is force invoiced after closed cutoff""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + cutoff.state = "done" + # Force invoiced after cutoff lines generated, cutoff is posted + self.so.force_invoiced = True + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + # Remove Force invoiced, nothing changes + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + amount = self.qty * self.price + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, amount, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_before_but_posted(self): + """Test cutoff when SO is force invoiced before closed cutoff""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + # Force invoiced before cutoff lines generated, lines should be deleted + self.so.force_invoiced = True + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") + cutoff.state = "done" + # Remove Force invoiced, lines should be created + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") + + def test_accrued_revenue_on_so_force_invoiced_line_added(self): + """Test cutoff when SO is force invoiced and line is added""" + cutoff = self.revenue_cutoff + self.so.action_confirm() + self.so.force_invoiced = True + p = self.env.ref("product.expense_product") + self.so.order_line = [ + Command.create( + { + "name": p.name, + "product_id": p.id, + "product_uom_qty": 1, + "product_uom": p.uom_id.id, + "price_unit": self.price, + "tax_id": [Command.set(self.tax_sale.ids)], + }, + ) + ] + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "0 cutoff line should be found") + for sol in self.so.order_line: + self.assertEqual(sol.is_cutoff_accrual_excluded, True) 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..c7ce3c61f2e --- /dev/null +++ b/account_cutoff_accrual_sale/views/account_cutoff.xml @@ -0,0 +1,31 @@ + + + + + + Accrued Revenue 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, +) From 7958d5e0b3c54aa725dfc1ed65a9a7cb225e051a Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 10 Oct 2024 17:45:36 +0200 Subject: [PATCH 4/7] [ADD] account_cutoff_accrual_sale_stock --- account_cutoff_accrual_sale_stock/README.rst | 89 ++++ account_cutoff_accrual_sale_stock/__init__.py | 4 + .../__manifest__.py | 21 + .../models/__init__.py | 4 + .../models/sale_order_line.py | 74 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 6 + .../static/description/index.html | 426 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/common.py | 34 ++ .../tests/test_cutoff_revenue.py | 294 ++++++++++++ .../addons/account_cutoff_accrual_sale_stock | 1 + .../setup.py | 6 + 13 files changed, 961 insertions(+) create mode 100644 account_cutoff_accrual_sale_stock/README.rst create mode 100644 account_cutoff_accrual_sale_stock/__init__.py create mode 100644 account_cutoff_accrual_sale_stock/__manifest__.py create mode 100644 account_cutoff_accrual_sale_stock/models/__init__.py create mode 100644 account_cutoff_accrual_sale_stock/models/sale_order_line.py create mode 100644 account_cutoff_accrual_sale_stock/readme/CONTRIBUTORS.rst create mode 100644 account_cutoff_accrual_sale_stock/readme/DESCRIPTION.rst create mode 100644 account_cutoff_accrual_sale_stock/static/description/index.html create mode 100644 account_cutoff_accrual_sale_stock/tests/__init__.py create mode 100644 account_cutoff_accrual_sale_stock/tests/common.py create mode 100644 account_cutoff_accrual_sale_stock/tests/test_cutoff_revenue.py create mode 120000 setup/account_cutoff_accrual_sale_stock/odoo/addons/account_cutoff_accrual_sale_stock create mode 100644 setup/account_cutoff_accrual_sale_stock/setup.py diff --git a/account_cutoff_accrual_sale_stock/README.rst b/account_cutoff_accrual_sale_stock/README.rst new file mode 100644 index 00000000000..42a6a804209 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/README.rst @@ -0,0 +1,89 @@ +================================== +Account Cut-off Accrual Sale Stock +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6cc8b7ea7831f689ded98624a38d367bd634a25ff068d030285e4e0c5a4e9bce + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--closing-lightgray.png?logo=github + :target: https://github.com/OCA/account-closing/tree/16.0/account_cutoff_accrual_sale_stock + :alt: OCA/account-closing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-closing-16-0/account-closing-16-0-account_cutoff_accrual_sale_stock + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-closing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of account_cutoff_accrual_sale +to allow the computation of revenue cutoffs on sales orders for storable products. + +This glue module allows to detect the quantities delivered after the cutoff date. + +See account_cutoff_accrual_sale for more explanations. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/account-closing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_cutoff_accrual_sale_stock/__init__.py b/account_cutoff_accrual_sale_stock/__init__.py new file mode 100644 index 00000000000..86337901b11 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/account_cutoff_accrual_sale_stock/__manifest__.py b/account_cutoff_accrual_sale_stock/__manifest__.py new file mode 100644 index 00000000000..cd33bb2caa0 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Account Cut-off Accrual Sale Stock", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "license": "AGPL-3", + "summary": "Glue module for Cut-Off Accruals on Sales with Stock Deliveries", + "author": "BCIM, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/account-closing", + "depends": [ + "account_cutoff_accrual_sale", + "account_cutoff_accrual_order_stock_base", + "sale_stock", + ], + "installable": True, + "application": False, + "auto_install": True, +} diff --git a/account_cutoff_accrual_sale_stock/models/__init__.py b/account_cutoff_accrual_sale_stock/models/__init__.py new file mode 100644 index 00000000000..b0c9d05c9d4 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import sale_order_line diff --git a/account_cutoff_accrual_sale_stock/models/sale_order_line.py b/account_cutoff_accrual_sale_stock/models/sale_order_line.py new file mode 100644 index 00000000000..ea3d7e5b436 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/models/sale_order_line.py @@ -0,0 +1,74 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM sprl) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class SaleOrderLine(models.Model): + _name = "sale.order.line" + _inherit = ["sale.order.line", "order.line.cutoff.accrual.mixin"] + + def _get_cutoff_accrual_lines_delivered_after(self, cutoff): + lines = super()._get_cutoff_accrual_lines_delivered_after(cutoff) + cutoff_nextday = cutoff._nextday_start_dt() + # Take all moves done after the cutoff date + # In SQL to reduce memory usage as we could process large dataset + self.env.cr.execute( + """ + SELECT order_id + FROM sale_order_line + WHERE id in ( + SELECT sale_line_id + FROM stock_move + WHERE state='done' + AND date >= %s + AND sale_line_id IS NOT NULL + ) + """, + (cutoff_nextday,), + ) + sale_ids = [x[0] for x in self.env.cr.fetchall()] + lines = self.env["sale.order.line"].search( + ["|", ("order_id", "in", sale_ids), ("id", "in", lines.ids)], order="id" + ) + return lines + + def _get_cutoff_accrual_delivered_min_date(self): + """Return first delivery date""" + self.ensure_one() + stock_moves = self.move_ids.filtered(lambda m: m.state == "done") + if not stock_moves: + return + return min(stock_moves.mapped("date")).date() + + def _get_cutoff_accrual_delivered_stock_quantity(self, cutoff): + self.ensure_one() + cutoff_nextday = cutoff._nextday_start_dt() + if self.create_date >= cutoff_nextday: + # A line added after the cutoff cannot be delivered in the past + return 0 + delivered_qty = self.qty_delivered + # The quantity delivered on the SO line must be deducted from all + # moves done after the cutoff date. + out_moves, in_moves = self._get_outgoing_incoming_moves() + for move in out_moves: + if move.state != "done" or move.date < cutoff_nextday: + continue + delivered_qty -= move.product_uom._compute_quantity( + move.product_uom_qty, + self.product_uom, + rounding_method="HALF-UP", + ) + for move in in_moves: + if move.state != "done" or move.date < cutoff_nextday: + continue + delivered_qty += move.product_uom._compute_quantity( + move.product_uom_qty, + self.product_uom, + rounding_method="HALF-UP", + ) + return delivered_qty diff --git a/account_cutoff_accrual_sale_stock/readme/CONTRIBUTORS.rst b/account_cutoff_accrual_sale_stock/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/account_cutoff_accrual_sale_stock/readme/DESCRIPTION.rst b/account_cutoff_accrual_sale_stock/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..9f66bb1ba96 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module extends the functionality of account_cutoff_accrual_sale +to allow the computation of revenue cutoffs on sales orders for storable products. + +This glue module allows to detect the quantities delivered after the cutoff date. + +See account_cutoff_accrual_sale for more explanations. diff --git a/account_cutoff_accrual_sale_stock/static/description/index.html b/account_cutoff_accrual_sale_stock/static/description/index.html new file mode 100644 index 00000000000..45b8e8a68cc --- /dev/null +++ b/account_cutoff_accrual_sale_stock/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +Account Cut-off Accrual Sale Stock + + + +
+

Account Cut-off Accrual Sale Stock

+ + +

Beta License: AGPL-3 OCA/account-closing Translate me on Weblate Try me on Runboat

+

This module extends the functionality of account_cutoff_accrual_sale +to allow the computation of revenue cutoffs on sales orders for storable products.

+

This glue module allows to detect the quantities delivered after the cutoff date.

+

See account_cutoff_accrual_sale for more explanations.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/account-closing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_cutoff_accrual_sale_stock/tests/__init__.py b/account_cutoff_accrual_sale_stock/tests/__init__.py new file mode 100644 index 00000000000..34d4dd52a80 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cutoff_revenue diff --git a/account_cutoff_accrual_sale_stock/tests/common.py b/account_cutoff_accrual_sale_stock/tests/common.py new file mode 100644 index 00000000000..45b91685806 --- /dev/null +++ b/account_cutoff_accrual_sale_stock/tests/common.py @@ -0,0 +1,34 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.addons.account_cutoff_accrual_sale.tests.common import ( + TestAccountCutoffAccrualSaleCommon, +) + + +class TestAccountCutoffAccrualSaleStockCommon(TestAccountCutoffAccrualSaleCommon): + def _confirm_so_and_do_picking(self, qty_done): + self.so.action_confirm() + # Make invoice what product on order + self.so._create_invoices(final=True) + self.assertEqual( + self.so.invoice_status, + "no", + 'SO invoice_status should be "nothing to invoice" after confirming', + ) + # Deliver + pick = self.so.picking_ids + pick.action_assign() + pick.move_line_ids.write({"qty_done": qty_done}) # receive 2/5 # deliver 2/5 + pick._action_done() + self.assertEqual( + self.so.invoice_status, + "to invoice", + 'SO invoice_status should be "to invoice" after partial delivery', + ) + qties = [sol.qty_delivered for sol in self.so.order_line] + self.assertEqual( + qties, + [qty_done if p.detailed_type == "product" else 0 for p in self.products], + "Delivered quantities are wrong after partial delivery", + ) diff --git a/account_cutoff_accrual_sale_stock/tests/test_cutoff_revenue.py b/account_cutoff_accrual_sale_stock/tests/test_cutoff_revenue.py new file mode 100644 index 00000000000..3c26f8689ce --- /dev/null +++ b/account_cutoff_accrual_sale_stock/tests/test_cutoff_revenue.py @@ -0,0 +1,294 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from .common import TestAccountCutoffAccrualSaleStockCommon + + +class TestAccountCutoffAccrualSale(TestAccountCutoffAccrualSaleStockCommon): + def test_accrued_revenue_empty(self): + """Test cutoff when there is no SO.""" + cutoff = self.revenue_cutoff + cutoff.get_lines() + self.assertEqual( + len(cutoff.line_ids), 0, "There should be no SO line to process" + ) + + def test_revenue_analytic_distribution(self): + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertDictEqual( + line.analytic_distribution, + {str(self.analytic_account.id): 100.0}, + "Analytic distribution is not correctly set", + ) + + def test_revenue_tax_line(self): + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + len(line.tax_line_ids), 1, "tax lines is not correctly set" + ) + self.assertEqual(line.tax_line_ids.cutoff_account_id, self.cutoff_account) + self.assertEqual(line.tax_line_ids.tax_id, self.tax_sale) + self.assertEqual(line.tax_line_ids.base, 200) + self.assertEqual(line.tax_line_ids.amount, 30) + self.assertEqual(line.tax_line_ids.cutoff_amount, 30) + + def test_accrued_revenue_on_so_not_invoiced(self): + """Test cutoff based on SO where qty_delivered > qty_invoiced.""" + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Make invoice + invoice = self.so._create_invoices(final=True) + # - invoice is in draft, no change to cutoff + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Validate invoice + invoice.action_post() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual(line.cutoff_amount, 0, "SO line cutoff amount incorrect") + # Make a refund - the refund reset the SO lines qty_invoiced + self._refund_invoice(invoice) + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual(line.cutoff_amount, 200, "SO line cutoff amount incorrect") + + def test_accrued_revenue_on_so_all_invoiced(self): + """Test cutoff based on SO where qty_delivered = qty_invoiced.""" + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + # Make invoice + invoice = self.so._create_invoices(final=True) + # Validate invoice + invoice.action_post() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 1, "1 cutoff line should be found") + # Make a refund - the refund reset qty_invoiced + self._refund_invoice(invoice) + self.assertEqual(len(cutoff.line_ids), 3, "No cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual(line.cutoff_amount, 200, "SO line cutoff amount incorrect") + + def test_accrued_revenue_on_so_draft_invoice(self): + """Test cutoff based on SO where qty_delivered = qty_invoiced but the. + + invoice is still in draft + """ + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + # Make invoice + invoice = self.so._create_invoices(final=True) + # - invoice is in draft, no change to cutoff + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Validate invoice + invoice.action_post() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual(line.cutoff_amount, 0, "SO line cutoff amount incorrect") + # Make a refund - the refund reset SO lines qty_invoiced + self._refund_invoice(invoice) + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual(line.cutoff_amount, 200, "SO line cutoff amount incorrect") + + def test_accrued_revenue_on_so_not_invoiced_after_cutoff(self): + """Test cutoff based on SO where qty_delivered > qty_invoiced. + + And make invoice after cutoff date + """ + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + cutoff.get_lines() + # Make invoice + invoice = self.so._create_invoices(final=True) + # - invoice is in draft, no change to cutoff + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Validate invoice after cutoff + invoice.invoice_date = cutoff.cutoff_date + timedelta(days=1) + invoice.action_post() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Make a refund after cutoff + refund = self._refund_invoice(invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_all_invoiced_after_cutoff(self): + """Test cutoff based on SO where qty_delivered = qty_invoiced. + + And make invoice after cutoff date + """ + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + # Make invoice + invoice = self.so._create_invoices(final=True) + # Validate invoice after cutoff + invoice.invoice_date = cutoff.cutoff_date + timedelta(days=1) + invoice.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 2 * 100, "SO line cutoff amount incorrect" + ) + # Make a refund - the refund reset SO lines qty_invoiced + refund = self._refund_invoice(invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_after(self): + """Test cutoff when SO is force invoiced after cutoff""" + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Force invoiced after cutoff lines generated, lines should be deleted + self.so.force_invoiced = True + self.assertEqual(len(cutoff.line_ids), 0, "cutoff line should deleted") + # Remove Force invoiced, lines should be recreated + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_before(self): + """Test cutoff when SO is force invoiced before cutoff""" + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + # Force invoiced before cutoff lines generated, lines should not be created + self.so.force_invoiced = True + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "No cutoff lines should be generated") + # Remove Force invoiced, lines should be created + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_after_but_posted(self): + """Test cutoff when SO is force invoiced after closed cutoff""" + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + cutoff.state = "done" + # Force invoiced after cutoff lines generated, cutoff is posted + self.so.force_invoiced = True + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + # Remove Force invoiced, nothing changes + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 3, "3 cutoff lines should be found") + for line in cutoff.line_ids.filtered( + lambda l: l.product_id.detailed_type == "product" + ): + self.assertEqual( + line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect" + ) + + def test_accrued_revenue_on_so_force_invoiced_before_but_posted(self): + """Test cutoff when SO is force invoiced before closed cutoff""" + cutoff = self.revenue_cutoff + self._confirm_so_and_do_picking(2) + # Force invoiced before cutoff lines generated, lines should be deleted + self.so.force_invoiced = True + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") + cutoff.state = "done" + # Remove Force invoiced, lines should be created + self.so.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") diff --git a/setup/account_cutoff_accrual_sale_stock/odoo/addons/account_cutoff_accrual_sale_stock b/setup/account_cutoff_accrual_sale_stock/odoo/addons/account_cutoff_accrual_sale_stock new file mode 120000 index 00000000000..f8b490af6fd --- /dev/null +++ b/setup/account_cutoff_accrual_sale_stock/odoo/addons/account_cutoff_accrual_sale_stock @@ -0,0 +1 @@ +../../../../account_cutoff_accrual_sale_stock \ No newline at end of file diff --git a/setup/account_cutoff_accrual_sale_stock/setup.py b/setup/account_cutoff_accrual_sale_stock/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_accrual_sale_stock/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 4e88c5449e6503e00141853caf230b48633abbca Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 10 Oct 2024 17:45:48 +0200 Subject: [PATCH 5/7] [ADD] account_cutoff_accrual_purchase --- account_cutoff_accrual_purchase/README.rst | 153 ++++++ account_cutoff_accrual_purchase/__init__.py | 5 + .../__manifest__.py | 26 + .../data/ir_cron.xml | 20 + account_cutoff_accrual_purchase/hooks.py | 13 + account_cutoff_accrual_purchase/i18n/fr.po | 82 +++ .../models/__init__.py | 8 + .../models/account_cutoff.py | 12 + .../models/account_cutoff_line.py | 26 + .../models/account_move.py | 15 + .../models/purchase_order.py | 14 + .../models/purchase_order_line.py | 102 ++++ .../readme/CONFIGURE.rst | 7 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 30 ++ .../readme/USAGE.rst | 25 + .../static/description/index.html | 486 ++++++++++++++++++ .../views/account_cutoff.xml | 31 ++ .../views/account_cutoff_line.xml | 22 + .../addons/account_cutoff_accrual_purchase | 1 + .../account_cutoff_accrual_purchase/setup.py | 6 + 21 files changed, 1085 insertions(+) create mode 100644 account_cutoff_accrual_purchase/README.rst create mode 100644 account_cutoff_accrual_purchase/__init__.py create mode 100644 account_cutoff_accrual_purchase/__manifest__.py create mode 100644 account_cutoff_accrual_purchase/data/ir_cron.xml create mode 100644 account_cutoff_accrual_purchase/hooks.py create mode 100644 account_cutoff_accrual_purchase/i18n/fr.po create mode 100644 account_cutoff_accrual_purchase/models/__init__.py create mode 100644 account_cutoff_accrual_purchase/models/account_cutoff.py create mode 100644 account_cutoff_accrual_purchase/models/account_cutoff_line.py create mode 100644 account_cutoff_accrual_purchase/models/account_move.py create mode 100644 account_cutoff_accrual_purchase/models/purchase_order.py create mode 100644 account_cutoff_accrual_purchase/models/purchase_order_line.py create mode 100644 account_cutoff_accrual_purchase/readme/CONFIGURE.rst create mode 100644 account_cutoff_accrual_purchase/readme/CONTRIBUTORS.rst create mode 100644 account_cutoff_accrual_purchase/readme/DESCRIPTION.rst create mode 100644 account_cutoff_accrual_purchase/readme/USAGE.rst create mode 100644 account_cutoff_accrual_purchase/static/description/index.html create mode 100644 account_cutoff_accrual_purchase/views/account_cutoff.xml create mode 100644 account_cutoff_accrual_purchase/views/account_cutoff_line.xml create mode 120000 setup/account_cutoff_accrual_purchase/odoo/addons/account_cutoff_accrual_purchase create mode 100644 setup/account_cutoff_accrual_purchase/setup.py diff --git a/account_cutoff_accrual_purchase/README.rst b/account_cutoff_accrual_purchase/README.rst new file mode 100644 index 00000000000..f396f214b98 --- /dev/null +++ b/account_cutoff_accrual_purchase/README.rst @@ -0,0 +1,153 @@ +================================ +Account Cut-off Accrual Purchase +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a0536b91a6e1c6035a5c0a21557f7bf3d7c392b59c460a6ab9ed1e40d9f6180c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--closing-lightgray.png?logo=github + :target: https://github.com/OCA/account-closing/tree/16.0/account_cutoff_accrual_purchase + :alt: OCA/account-closing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-closing-16-0/account-closing-16-0-account_cutoff_accrual_purchase + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-closing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of account_cutoff_accrual_order_base +to allow the computation of expense cutoffs on purchase 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 PO, you can make the difference between: +* invoice to receive (received qty > invoiced qty) +* goods to receive (prepayment) (received qty < invoiced qty) + +If you expect a refund, you can make it in draft. In standard, this update +the PO and the quantity will not be accrued as goods to receive. You can accrue +the draft credit note as "credit notes to receive". + +Orders forced in status invoiced won't have cutoff entries. +For instance, if you know you will never receive the missing invoiced goods, +you can force it as invoiced. + +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. +Nevertheless, you can still reset to draft a supplier invoice but you won't be +able to modify any amount. You are then supposed to re-validate the invoice. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +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. + +Usage +===== + +To use this module, you need to: + +#. Go to Accounting - Cut-offs to configure and generate the cutoffs + +Examples +======== + +* Purchase Order with quantity received: 0, quantity invoiced: 0 + This will not make an cutoff entry + +* Purchase Order with quantity received: 10, quantity invoiced: 0 + This will make an cutoff entry with invoice to receive: 10 + +* Purchase Order with quantity received: 0, quantity invoiced: 10 + This will make an cutoff entry with goods to receive: 10 + +* Purchase Order with quantity received: 10, quantity invoiced: 0 + This will make an cutoff entry with invoice to receive: 10 + Invoice is encoded after the cut-off date but dated before the cut-off date + The cutoff entry is updated in the existing cut-off + +* Purchase Order with quantity received: 0, quantity invoiced: 0 + This will not make an cutoff entry + Invoice is encoded after the cut-off date but dated before the cut-off date + An cutoff entry is added in the existing cut-off + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/account-closing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_cutoff_accrual_purchase/__init__.py b/account_cutoff_accrual_purchase/__init__.py new file mode 100644 index 00000000000..49f04f052cf --- /dev/null +++ b/account_cutoff_accrual_purchase/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from .hooks import post_init_hook diff --git a/account_cutoff_accrual_purchase/__manifest__.py b/account_cutoff_accrual_purchase/__manifest__.py new file mode 100644 index 00000000000..7ca21b8d5ae --- /dev/null +++ b/account_cutoff_accrual_purchase/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Account Cut-off Accrual Purchase", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "license": "AGPL-3", + "summary": "Accrued Expense on Purchase Order", + "author": "BCIM, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/account-closing", + "depends": [ + "account_cutoff_accrual_order_base", + "purchase", + "purchase_force_invoiced", + ], + "data": [ + "views/account_cutoff.xml", + "views/account_cutoff_line.xml", + "data/ir_cron.xml", + ], + "post_init_hook": "post_init_hook", + "installable": True, + "application": True, +} diff --git a/account_cutoff_accrual_purchase/data/ir_cron.xml b/account_cutoff_accrual_purchase/data/ir_cron.xml new file mode 100644 index 00000000000..f0670c724d6 --- /dev/null +++ b/account_cutoff_accrual_purchase/data/ir_cron.xml @@ -0,0 +1,20 @@ + + + + + Make cutoff at end of period - purchase order lines + + + code + model._cron_cutoff("accrued_expense", "purchase.order.line") + + 1 + months + -1 + + + + + diff --git a/account_cutoff_accrual_purchase/hooks.py b/account_cutoff_accrual_purchase/hooks.py new file mode 100644 index 00000000000..26b056b49d4 --- /dev/null +++ b/account_cutoff_accrual_purchase/hooks.py @@ -0,0 +1,13 @@ +# Copyright 2023 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + + +def post_init_hook(cr, registry): + cr.execute( + """ + UPDATE purchase_order_line + SET is_cutoff_accrual_excluded = TRUE + WHERE order_id IN + ( SELECT id FROM purchase_order WHERE force_invoiced ) + """ + ) diff --git a/account_cutoff_accrual_purchase/i18n/fr.po b/account_cutoff_accrual_purchase/i18n/fr.po new file mode 100644 index 00000000000..05cd3547483 --- /dev/null +++ b/account_cutoff_accrual_purchase/i18n/fr.po @@ -0,0 +1,82 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_cutoff_accrual_purchase +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 11:44+0000\n" +"PO-Revision-Date: 2023-10-26 11:44+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model,name:account_cutoff_accrual_purchase.model_account_cutoff +msgid "Account Cut-off" +msgstr "Provision" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model,name:account_cutoff_accrual_purchase.model_account_cutoff_line +msgid "Account Cut-off Line" +msgstr "Ligne de provision" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model.fields,field_description:account_cutoff_accrual_purchase.field_purchase_order_line__account_cutoff_line_ids +msgid "Account Cutoff Lines" +msgstr "Lignes de provision" + +#. module: account_cutoff_accrual_purchase +#: model:ir.actions.act_window,name:account_cutoff_accrual_purchase.account_accrual_action +#: model:ir.ui.menu,name:account_cutoff_accrual_purchase.account_accrual_menu +msgid "Accrued Expense on Purchase Orders" +msgstr "Provisions sur Achats" + +#. module: account_cutoff_accrual_purchase +#: model_terms:ir.actions.act_window,help:account_cutoff_accrual_purchase.account_accrual_action +msgid "Click to start preparing a new expense accrual." +msgstr "" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model,name:account_cutoff_accrual_purchase.model_account_move +msgid "Journal Entry" +msgstr "Pièce comptable" + +#. module: account_cutoff_accrual_purchase +#: model:ir.actions.server,name:account_cutoff_accrual_purchase.ir_cron_cutoff_ir_actions_server +#: model:ir.cron,cron_name:account_cutoff_accrual_purchase.ir_cron_cutoff +msgid "Make cutoff at end of period - purchase order lines" +msgstr "" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model.fields,field_description:account_cutoff_accrual_purchase.field_account_cutoff__order_line_model +msgid "Order Line Model" +msgstr "" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model.fields,field_description:account_cutoff_accrual_purchase.field_account_cutoff_line__purchase_order_id +msgid "Order Reference" +msgstr "Commande" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model,name:account_cutoff_accrual_purchase.model_purchase_order_line +#: model:ir.model.fields,field_description:account_cutoff_accrual_purchase.field_account_cutoff_line__purchase_line_id +msgid "Purchase Order Line" +msgstr "Ligne de commande d'achat" + +#. module: account_cutoff_accrual_purchase +#: model:ir.model.fields.selection,name:account_cutoff_accrual_purchase.selection__account_cutoff__order_line_model__purchase_order_line +msgid "Purchase Orders" +msgstr "Achats" + +#. module: account_cutoff_accrual_purchase +#: model_terms:ir.actions.act_window,help:account_cutoff_accrual_purchase.account_accrual_action +msgid "" +"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." +msgstr "" diff --git a/account_cutoff_accrual_purchase/models/__init__.py b/account_cutoff_accrual_purchase/models/__init__.py new file mode 100644 index 00000000000..1bd7de13f64 --- /dev/null +++ b/account_cutoff_accrual_purchase/models/__init__.py @@ -0,0 +1,8 @@ +# 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 purchase_order +from . import purchase_order_line +from . import account_move diff --git a/account_cutoff_accrual_purchase/models/account_cutoff.py b/account_cutoff_accrual_purchase/models/account_cutoff.py new file mode 100644 index 00000000000..4f8f84979be --- /dev/null +++ b/account_cutoff_accrual_purchase/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=[("purchase.order.line", "Purchase Orders")] + ) diff --git a/account_cutoff_accrual_purchase/models/account_cutoff_line.py b/account_cutoff_accrual_purchase/models/account_cutoff_line.py new file mode 100644 index 00000000000..0e4c5dadb75 --- /dev/null +++ b/account_cutoff_accrual_purchase/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" + + purchase_line_id = fields.Many2one( + comodel_name="purchase.order.line", string="Purchase Order Line", readonly=True + ) + purchase_order_id = fields.Many2one(related="purchase_line_id.order_id") + + def _get_order_line(self): + if self.purchase_line_id: + return self.purchase_line_id + return super()._get_order_line() + + @api.depends("purchase_line_id") + def _compute_invoice_lines(self): + for rec in self: + if rec.purchase_line_id: + rec.invoice_line_ids = rec.purchase_line_id.invoice_lines + super()._compute_invoice_lines() + return diff --git a/account_cutoff_accrual_purchase/models/account_move.py b/account_cutoff_accrual_purchase/models/account_move.py new file mode 100644 index 00000000000..7f0f8c83c7e --- /dev/null +++ b/account_cutoff_accrual_purchase/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 ("in_invoice", "in_refund"): + res.append(self.invoice_line_ids.purchase_line_id) + return res diff --git a/account_cutoff_accrual_purchase/models/purchase_order.py b/account_cutoff_accrual_purchase/models/purchase_order.py new file mode 100644 index 00000000000..84337c442eb --- /dev/null +++ b/account_cutoff_accrual_purchase/models/purchase_order.py @@ -0,0 +1,14 @@ +# Copyright 2023 Jacques-Etienne Baudoux (BCIM sprl) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + def write(self, vals): + res = super().write(vals) + if "force_invoiced" in vals: + self.order_line.is_cutoff_accrual_excluded = vals["force_invoiced"] + return res diff --git a/account_cutoff_accrual_purchase/models/purchase_order_line.py b/account_cutoff_accrual_purchase/models/purchase_order_line.py new file mode 100644 index 00000000000..02e7907e18d --- /dev/null +++ b/account_cutoff_accrual_purchase/models/purchase_order_line.py @@ -0,0 +1,102 @@ +# 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 PurchaseOrderLine(models.Model): + _name = "purchase.order.line" + _inherit = ["purchase.order.line", "order.line.cutoff.accrual.mixin"] + + account_cutoff_line_ids = fields.One2many( + "account.cutoff.line", + "purchase_line_id", + string="Account Cutoff Lines", + readonly=True, + ) + + is_cutoff_accrual_excluded = fields.Boolean( + compute="_compute_is_cutoff_accrual_excluded", + store=True, + ) + + @api.depends("order_id.force_invoiced") + def _compute_is_cutoff_accrual_excluded(self): + for rec in self: + rec.is_cutoff_accrual_excluded = rec.order_id.force_invoiced + + def _get_cutoff_accrual_lines_domain(self, cutoff): + domain = super()._get_cutoff_accrual_lines_domain(cutoff) + domain.append(("order_id.state", "in", ("purchase", "done"))) + domain.append(("order_id.invoice_status", "!=", "invoiced")) + return domain + + @api.model + def _get_cutoff_accrual_lines_query(self, cutoff): + query = super()._get_cutoff_accrual_lines_query(cutoff) + self.flush_model(["display_type", "qty_received", "qty_invoiced"]) + query.add_where( + f'"{self._table}".display_type IS NULL AND ' + f'"{self._table}".qty_received != "{self._table}".qty_invoiced' + ) + return query + + def _prepare_cutoff_accrual_line(self, cutoff): + res = super()._prepare_cutoff_accrual_line(cutoff) + if not res: + return + res["purchase_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 = [ + ("purchase_line_id.is_cutoff_accrual_excluded", "!=", True), + ("move_id.move_type", "in", ("in_invoice", "in_refund")), + ("purchase_line_id", "!=", False), + "|", + ("move_id.state", "=", "draft"), + "&", + ("move_id.state", "=", "posted"), + ("move_id.date", ">=", cutoff_nextday), + ] + invoice_line_after = self.env["account.move.line"].search(domain, order="id") + _logger.debug( + "Purchase Invoice Lines done after cutoff: %s" % len(invoice_line_after) + ) + if not invoice_line_after: + return self.env["purchase.order.line"] + # In SQL to reduce memory usage as we could process large dataset + self.env.cr.execute( + """ + SELECT order_id + FROM purchase_order_line + WHERE id in ( + SELECT purchase_line_id + FROM account_move_line + WHERE id in %s + ) + """, + (tuple(invoice_line_after.ids),), + ) + purchase_ids = [x[0] for x in self.env.cr.fetchall()] + lines = self.env["purchase.order.line"].search( + [("order_id", "in", purchase_ids)], order="id" + ) + return lines + + def _get_cutoff_accrual_delivered_service_quantity(self, cutoff): + # By default, no cutoff on purchase. Set received as invoiced. + self.ensure_one() + return self._get_cutoff_accrual_invoiced_quantity(cutoff) + + def _get_cutoff_accrual_delivered_stock_quantity(self, cutoff): + # By default, no cutoff on purchase. Set received as invoiced. + self.ensure_one() + return self._get_cutoff_accrual_invoiced_quantity(cutoff) diff --git a/account_cutoff_accrual_purchase/readme/CONFIGURE.rst b/account_cutoff_accrual_purchase/readme/CONFIGURE.rst new file mode 100644 index 00000000000..b7ad4bec8a9 --- /dev/null +++ b/account_cutoff_accrual_purchase/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_purchase/readme/CONTRIBUTORS.rst b/account_cutoff_accrual_purchase/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/account_cutoff_accrual_purchase/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/account_cutoff_accrual_purchase/readme/DESCRIPTION.rst b/account_cutoff_accrual_purchase/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..26e68f496cc --- /dev/null +++ b/account_cutoff_accrual_purchase/readme/DESCRIPTION.rst @@ -0,0 +1,30 @@ +This module extends the functionality of account_cutoff_accrual_order_base +to allow the computation of expense cutoffs on purchase 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 PO, you can make the difference between: +* invoice to receive (received qty > invoiced qty) +* goods to receive (prepayment) (received qty < invoiced qty) + +If you expect a refund, you can make it in draft. In standard, this update +the PO and the quantity will not be accrued as goods to receive. You can accrue +the draft credit note as "credit notes to receive". + +Orders forced in status invoiced won't have cutoff entries. +For instance, if you know you will never receive the missing invoiced goods, +you can force it as invoiced. + +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. +Nevertheless, you can still reset to draft a supplier invoice but you won't be +able to modify any amount. You are then supposed to re-validate the invoice. diff --git a/account_cutoff_accrual_purchase/readme/USAGE.rst b/account_cutoff_accrual_purchase/readme/USAGE.rst new file mode 100644 index 00000000000..10a277a282f --- /dev/null +++ b/account_cutoff_accrual_purchase/readme/USAGE.rst @@ -0,0 +1,25 @@ +To use this module, you need to: + +#. Go to Accounting - Cut-offs to configure and generate the cutoffs + +Examples +======== + +* Purchase Order with quantity received: 0, quantity invoiced: 0 + This will not make an cutoff entry + +* Purchase Order with quantity received: 10, quantity invoiced: 0 + This will make an cutoff entry with invoice to receive: 10 + +* Purchase Order with quantity received: 0, quantity invoiced: 10 + This will make an cutoff entry with goods to receive: 10 + +* Purchase Order with quantity received: 10, quantity invoiced: 0 + This will make an cutoff entry with invoice to receive: 10 + Invoice is encoded after the cut-off date but dated before the cut-off date + The cutoff entry is updated in the existing cut-off + +* Purchase Order with quantity received: 0, quantity invoiced: 0 + This will not make an cutoff entry + Invoice is encoded after the cut-off date but dated before the cut-off date + An cutoff entry is added in the existing cut-off diff --git a/account_cutoff_accrual_purchase/static/description/index.html b/account_cutoff_accrual_purchase/static/description/index.html new file mode 100644 index 00000000000..63b665070aa --- /dev/null +++ b/account_cutoff_accrual_purchase/static/description/index.html @@ -0,0 +1,486 @@ + + + + + + +Account Cut-off Accrual Purchase + + + +
+

Account Cut-off Accrual Purchase

+ + +

Beta License: AGPL-3 OCA/account-closing Translate me on Weblate Try me on Runboat

+

This module extends the functionality of account_cutoff_accrual_order_base +to allow the computation of expense cutoffs on purchase 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 PO, you can make the difference between: +* invoice to receive (received qty > invoiced qty) +* goods to receive (prepayment) (received qty < invoiced qty)

+

If you expect a refund, you can make it in draft. In standard, this update +the PO and the quantity will not be accrued as goods to receive. You can accrue +the draft credit note as “credit notes to receive”.

+

Orders forced in status invoiced won’t have cutoff entries. +For instance, if you know you will never receive the missing invoiced goods, +you can force it as invoiced.

+

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. +Nevertheless, you can still reset to draft a supplier invoice but you won’t be +able to modify any amount. You are then supposed to re-validate the invoice.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to the accounting settings to select the journals and accounts used for +the cutoff.
  2. +
  3. Analytic accounting needs to be enable in Accounting - Settings.
  4. +
  5. If you want to also accrue the taxes, you need in Accounting - Taxes, for +each type of taxes an accrued tax account.
  6. +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Accounting - Cut-offs to configure and generate the cutoffs
  2. +
+
+
+

Examples

+
    +
  • Purchase Order with quantity received: 0, quantity invoiced: 0 +This will not make an cutoff entry
  • +
  • Purchase Order with quantity received: 10, quantity invoiced: 0 +This will make an cutoff entry with invoice to receive: 10
  • +
  • Purchase Order with quantity received: 0, quantity invoiced: 10 +This will make an cutoff entry with goods to receive: 10
  • +
  • Purchase Order with quantity received: 10, quantity invoiced: 0 +This will make an cutoff entry with invoice to receive: 10 +Invoice is encoded after the cut-off date but dated before the cut-off date +The cutoff entry is updated in the existing cut-off
  • +
  • Purchase Order with quantity received: 0, quantity invoiced: 0 +This will not make an cutoff entry +Invoice is encoded after the cut-off date but dated before the cut-off date +An cutoff entry is added in the existing cut-off
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/account-closing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_cutoff_accrual_purchase/views/account_cutoff.xml b/account_cutoff_accrual_purchase/views/account_cutoff.xml new file mode 100644 index 00000000000..d2249f3ffa0 --- /dev/null +++ b/account_cutoff_accrual_purchase/views/account_cutoff.xml @@ -0,0 +1,31 @@ + + + + + + Accrued Expense on Purchase Orders + account.cutoff + tree,form + [('order_line_model', '=', 'purchase.order.line')] + {'default_order_line_model': 'purchase.order.line', 'default_cutoff_type': 'accrued_expense'} + +

+ Click to start preparing a new expense 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_purchase/views/account_cutoff_line.xml b/account_cutoff_accrual_purchase/views/account_cutoff_line.xml new file mode 100644 index 00000000000..ef2e9d4a64b --- /dev/null +++ b/account_cutoff_accrual_purchase/views/account_cutoff_line.xml @@ -0,0 +1,22 @@ + + + + + + account.cutoff.line + + + + + + + + + + diff --git a/setup/account_cutoff_accrual_purchase/odoo/addons/account_cutoff_accrual_purchase b/setup/account_cutoff_accrual_purchase/odoo/addons/account_cutoff_accrual_purchase new file mode 120000 index 00000000000..70da65fad23 --- /dev/null +++ b/setup/account_cutoff_accrual_purchase/odoo/addons/account_cutoff_accrual_purchase @@ -0,0 +1 @@ +../../../../account_cutoff_accrual_purchase \ No newline at end of file diff --git a/setup/account_cutoff_accrual_purchase/setup.py b/setup/account_cutoff_accrual_purchase/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_accrual_purchase/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From ede447142f3a12e9167c030f981171478c70f3bb Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 10 Oct 2024 17:45:54 +0200 Subject: [PATCH 6/7] [ADD] account_cutoff_accrual_purchase_stock --- .../README.rst | 89 ++++ .../__init__.py | 4 + .../__manifest__.py | 21 + .../models/__init__.py | 4 + .../models/purchase_order_line.py | 74 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 6 + .../static/description/index.html | 426 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/common.py | 92 ++++ .../tests/test_cutoff_expense.py | 329 ++++++++++++++ .../account_cutoff_accrual_purchase_stock | 1 + .../setup.py | 6 + 13 files changed, 1054 insertions(+) create mode 100644 account_cutoff_accrual_purchase_stock/README.rst create mode 100644 account_cutoff_accrual_purchase_stock/__init__.py create mode 100644 account_cutoff_accrual_purchase_stock/__manifest__.py create mode 100644 account_cutoff_accrual_purchase_stock/models/__init__.py create mode 100644 account_cutoff_accrual_purchase_stock/models/purchase_order_line.py create mode 100644 account_cutoff_accrual_purchase_stock/readme/CONTRIBUTORS.rst create mode 100644 account_cutoff_accrual_purchase_stock/readme/DESCRIPTION.rst create mode 100644 account_cutoff_accrual_purchase_stock/static/description/index.html create mode 100644 account_cutoff_accrual_purchase_stock/tests/__init__.py create mode 100644 account_cutoff_accrual_purchase_stock/tests/common.py create mode 100644 account_cutoff_accrual_purchase_stock/tests/test_cutoff_expense.py create mode 120000 setup/account_cutoff_accrual_purchase_stock/odoo/addons/account_cutoff_accrual_purchase_stock create mode 100644 setup/account_cutoff_accrual_purchase_stock/setup.py diff --git a/account_cutoff_accrual_purchase_stock/README.rst b/account_cutoff_accrual_purchase_stock/README.rst new file mode 100644 index 00000000000..7cfb4f78ead --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/README.rst @@ -0,0 +1,89 @@ +====================================== +Account Cut-off Accrual Purchase Stock +====================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a38a09f71438e61874c41e5707f0d98e49c5fe9b87cfaadbbcd2e31a0f9ca227 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--closing-lightgray.png?logo=github + :target: https://github.com/OCA/account-closing/tree/16.0/account_cutoff_accrual_purchase_stock + :alt: OCA/account-closing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-closing-16-0/account-closing-16-0-account_cutoff_accrual_purchase_stock + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-closing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of account_cutoff_accrual_purchase +to allow the computation of expense cutoffs on purchase orders for storable products. + +This glue module allows to detect the quantities received after the cutoff date. + +See account_cutoff_accrual_purchase for more explanations. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/account-closing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_cutoff_accrual_purchase_stock/__init__.py b/account_cutoff_accrual_purchase_stock/__init__.py new file mode 100644 index 00000000000..86337901b11 --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/account_cutoff_accrual_purchase_stock/__manifest__.py b/account_cutoff_accrual_purchase_stock/__manifest__.py new file mode 100644 index 00000000000..352a11d6e1e --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Account Cut-off Accrual Purchase Stock", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "license": "AGPL-3", + "summary": "Accrued Order Base", + "author": "BCIM, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/account-closing", + "depends": [ + "account_cutoff_accrual_purchase", + "account_cutoff_accrual_order_stock_base", + "purchase_stock", + ], + "installable": True, + "application": False, + "auto_install": True, +} diff --git a/account_cutoff_accrual_purchase_stock/models/__init__.py b/account_cutoff_accrual_purchase_stock/models/__init__.py new file mode 100644 index 00000000000..55312c475df --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import purchase_order_line diff --git a/account_cutoff_accrual_purchase_stock/models/purchase_order_line.py b/account_cutoff_accrual_purchase_stock/models/purchase_order_line.py new file mode 100644 index 00000000000..917e0c59faf --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/models/purchase_order_line.py @@ -0,0 +1,74 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM sprl) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class PurchaseOrderLine(models.Model): + _name = "purchase.order.line" + _inherit = ["purchase.order.line"] + + def _get_cutoff_accrual_lines_delivered_after(self, cutoff): + lines = super()._get_cutoff_accrual_lines_delivered_after(cutoff) + cutoff_nextday = cutoff._nextday_start_dt() + # Take all moves done after the cutoff date + # In SQL to reduce memory usage as we could process large dataset + self.env.cr.execute( + """ + SELECT order_id + FROM purchase_order_line + WHERE id in ( + SELECT purchase_line_id + FROM stock_move + WHERE state='done' + AND date >= %s + AND sale_line_id IS NOT NULL + ) + """, + (cutoff_nextday,), + ) + purchase_ids = [x[0] for x in self.env.cr.fetchall()] + lines = self.env["purchase.order.line"].search( + ["|", ("order_id", "in", purchase_ids), ("id", "in", lines.ids)], order="id" + ) + return lines + + def _get_cutoff_accrual_delivered_min_date(self): + """Return first delivery date""" + self.ensure_one() + stock_moves = self.move_ids.filtered(lambda m: m.state == "done") + if not stock_moves: + return + return min(stock_moves.mapped("date")).date() + + def _get_cutoff_accrual_delivered_stock_quantity(self, cutoff): + self.ensure_one() + cutoff_nextday = cutoff._nextday_start_dt() + if self.create_date >= cutoff_nextday: + # A line added after the cutoff cannot be received in the past + return 0 + received_qty = self.qty_received + # The quantity received on the PO line must be deducted from all + # moves done after the cutoff date. + out_moves, in_moves = self._get_outgoing_incoming_moves() + for move in out_moves: + if move.state != "done" or move.date < cutoff_nextday: + continue + received_qty += move.product_uom._compute_quantity( + move.product_uom_qty, + self.product_uom, + rounding_method="HALF-UP", + ) + for move in in_moves: + if move.state != "done" or move.date < cutoff_nextday: + continue + received_qty -= move.product_uom._compute_quantity( + move.product_uom_qty, + self.product_uom, + rounding_method="HALF-UP", + ) + return received_qty diff --git a/account_cutoff_accrual_purchase_stock/readme/CONTRIBUTORS.rst b/account_cutoff_accrual_purchase_stock/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/account_cutoff_accrual_purchase_stock/readme/DESCRIPTION.rst b/account_cutoff_accrual_purchase_stock/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..a93abc545a3 --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module extends the functionality of account_cutoff_accrual_purchase +to allow the computation of expense cutoffs on purchase orders for storable products. + +This glue module allows to detect the quantities received after the cutoff date. + +See account_cutoff_accrual_purchase for more explanations. diff --git a/account_cutoff_accrual_purchase_stock/static/description/index.html b/account_cutoff_accrual_purchase_stock/static/description/index.html new file mode 100644 index 00000000000..6458d77b0e2 --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +Account Cut-off Accrual Purchase Stock + + + +
+

Account Cut-off Accrual Purchase Stock

+ + +

Beta License: AGPL-3 OCA/account-closing Translate me on Weblate Try me on Runboat

+

This module extends the functionality of account_cutoff_accrual_purchase +to allow the computation of expense cutoffs on purchase orders for storable products.

+

This glue module allows to detect the quantities received after the cutoff date.

+

See account_cutoff_accrual_purchase for more explanations.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/account-closing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_cutoff_accrual_purchase_stock/tests/__init__.py b/account_cutoff_accrual_purchase_stock/tests/__init__.py new file mode 100644 index 00000000000..a6a5b7582d6 --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cutoff_expense diff --git a/account_cutoff_accrual_purchase_stock/tests/common.py b/account_cutoff_accrual_purchase_stock/tests/common.py new file mode 100644 index 00000000000..4d9ed33436f --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/tests/common.py @@ -0,0 +1,92 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import Command, fields +from odoo.tests.common import Form + +from odoo.addons.account_cutoff_accrual_order_base.tests.common import ( + TestAccountCutoffAccrualOrderCommon, +) + + +class TestAccountCutoffAccrualPurchaseCommon(TestAccountCutoffAccrualOrderCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Removing all existing PO + cls.env.cr.execute("DELETE FROM purchase_order;") + # Create PO + cls.tax_purchase = cls.env.company.account_purchase_tax_id + cls.cutoff_account = cls.env["account.account"].create( + { + "name": "account accrued expense", + "code": "accountAccruedExpense", + "account_type": "asset_current", + "company_id": cls.env.company.id, + } + ) + cls.tax_purchase.account_accrued_expense_id = cls.cutoff_account + cls.po = cls.env["purchase.order"].create( + { + "partner_id": cls.partner.id, + "order_line": [ + Command.create( + { + "name": p.name, + "product_id": p.id, + "product_qty": 5, + "product_uom": p.uom_po_id.id, + "price_unit": 100, + "date_planned": fields.Date.to_string( + datetime.today() + relativedelta(days=-15) + ), + "analytic_distribution": { + str(cls.analytic_account.id): 100.0 + }, + "taxes_id": [Command.set(cls.tax_purchase.ids)], + }, + ) + for p in cls.products + ], + } + ) + type_cutoff = "accrued_expense" + cls.expense_cutoff = ( + cls.env["account.cutoff"] + .with_context(default_cutoff_type=type_cutoff) + .create( + { + "cutoff_type": type_cutoff, + "order_line_model": "purchase.order.line", + "company_id": 1, + "cutoff_date": fields.Date.today(), + } + ) + ) + + def _confirm_po_and_do_picking(self, qty_done): + self.po.button_confirm() + self.po.button_approve(force=True) + pick = self.po.picking_ids + pick.action_assign() + pick.move_line_ids.write({"qty_done": qty_done}) + pick._action_done() + qties = [pol.qty_received for pol in self.po.order_line] + self.assertEqual( + qties, + [qty_done if p.detailed_type == "product" else 0 for p in self.products], + "Delivered quantities are wrong after partial delivery", + ) + + def _create_po_invoice(self, date): + invoice_form = Form( + self.env["account.move"].with_context( + default_move_type="in_invoice", default_purchase_id=self.po.id + ) + ) + invoice_form.invoice_date = date + return invoice_form.save() diff --git a/account_cutoff_accrual_purchase_stock/tests/test_cutoff_expense.py b/account_cutoff_accrual_purchase_stock/tests/test_cutoff_expense.py new file mode 100644 index 00000000000..073901f810d --- /dev/null +++ b/account_cutoff_accrual_purchase_stock/tests/test_cutoff_expense.py @@ -0,0 +1,329 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo import fields + +from .common import TestAccountCutoffAccrualPurchaseCommon + + +class TestAccountCutoffAccrualPurchase(TestAccountCutoffAccrualPurchaseCommon): + def test_accrued_expense_empty(self): + """Test cutoff when there is no PO.""" + cutoff = self.expense_cutoff + cutoff.get_lines() + self.assertEqual( + len(cutoff.line_ids), 0, "There should be no PO line to process" + ) + + def test_expense_analytic_distribution(self): + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertDictEqual( + line.analytic_distribution, + {str(self.analytic_account.id): 100.0}, + "Analytic distribution is not correctly set", + ) + + def test_expense_tax_line(self): + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + len(line.tax_line_ids), 1, "tax lines is not correctly set" + ) + self.assertEqual(line.tax_line_ids.cutoff_account_id, self.cutoff_account) + self.assertEqual(line.tax_line_ids.tax_id, self.tax_purchase) + self.assertEqual(line.tax_line_ids.base, 200) + self.assertEqual(line.tax_line_ids.amount, -30) + self.assertEqual(line.tax_line_ids.cutoff_amount, -30) + + def test_accrued_expense_on_po_not_invoiced(self): + """Test cutoff based on PO where qty_received > qty_invoiced.""" + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Make invoice + po_invoice = self._create_po_invoice(fields.Date.today()) + # - invoice is in draft, no change to cutoff + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Validate invoice + po_invoice.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "no cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "SO line cutoff amount incorrect") + # Make a refund after cutoff - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "no cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "SO line cutoff amount incorrect") + # Make a refund before cutoff + # - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice) + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_all_invoiced(self): + """Test cutoff based on PO where qty_received = qty_invoiced.""" + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + # Make invoice + po_invoice = self._create_po_invoice(fields.Date.today()) + # Validate invoice + po_invoice.action_post() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "No cutoff lines should be found") + # Make a refund after cutoff - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 0, "0 cutoff lines should be found") + # Make a refund before cutoff + # - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice) + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_draft_invoice(self): + """Test cutoff based on PO where qty_received = qty_invoiced but the. + + invoice is still in draft + """ + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + # Make invoice + po_invoice = self._create_po_invoice(fields.Date.today()) + # - invoice is in draft, cutoff generated + self.assertEqual(po_invoice.state, "draft", "invoice should be draft") + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Validate invoice + po_invoice.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "PO line cutoff amount incorrect") + # Make a refund after cutoff - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "PO line cutoff amount incorrect") + # Make a refund before cutoff + # - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice) + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_draft_refund(self): + """Test cutoff based on PO where qty_received = qty_invoiced but the. + + refund is still in draft + """ + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + # Make invoice for 5 + po_invoice = self._create_po_invoice(fields.Date.today()) + po_invoice.invoice_line_ids.write({"quantity": 5}) + # Validate invoice + po_invoice.action_post() + # Make a refund for the 3 that have not been received + # - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice, post=False) + refund.invoice_line_ids.write({"quantity": 3}) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "No cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "PO line cutoff amount incorrect") + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 0, "No cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual(line.cutoff_amount, 0, "PO line cutoff amount incorrect") + + def test_accrued_expense_on_po_not_invoiced_after_cutoff(self): + """Test cutoff based on PO where qty_received > qty_invoiced. + + And make invoice after cutoff date + """ + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + cutoff.get_lines() + + # Make invoice + po_invoice = self._create_po_invoice(fields.Date.today()) + # - invoice is in draft, no change to cutoff + + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Validate invoice after cutoff + po_invoice.date = cutoff.cutoff_date + timedelta(days=1) + po_invoice.action_post() + + self.assertEqual(len(cutoff.line_ids), 2, "no cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Make a refund after cutoff - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Make a refund before cutoff + # - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice) + + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_all_invoiced_after_cutoff(self): + """Test cutoff based on PO where qty_received = qty_invoiced. + + And make invoice after cutoff date + """ + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + # Make invoice + po_invoice = self._create_po_invoice(fields.Date.today()) + # Validate invoice after cutoff + po_invoice.date = cutoff.cutoff_date + timedelta(days=1) + po_invoice.action_post() + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Make a refund after cutoff - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice, post=False) + refund.date = cutoff.cutoff_date + timedelta(days=1) + refund.action_post() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Make a refund before cutoff + # - the refund is affecting the PO lines qty_invoiced + refund = self._refund_invoice(po_invoice) + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_force_invoiced_after(self): + """Test cutoff when PO is force invoiced after cutoff""" + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Force invoiced after cutoff lines generated, lines should be deleted + self.po.force_invoiced = True + self.assertEqual(len(cutoff.line_ids), 0, "cutoff line should deleted") + # Remove Force invoiced, lines should be recreated + self.po.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_force_invoiced_before(self): + """Test cutoff when PO is force invoiced before cutoff""" + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + # Force invoiced before cutoff lines generated, lines should be deleted + self.po.force_invoiced = True + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") + # Remove Force invoiced, lines should be created + self.po.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_force_invoiced_after_but_posted(self): + """Test cutoff when PO is force invoiced after closed cutoff""" + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + cutoff.state = "done" + # Force invoiced after cutoff lines generated, cutoff is posted + self.po.force_invoiced = True + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + # Remove Force invoiced, nothing changes + self.po.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found") + for line in cutoff.line_ids: + self.assertEqual( + line.cutoff_amount, -100 * 2, "PO line cutoff amount incorrect" + ) + + def test_accrued_expense_on_po_force_invoiced_before_but_posted(self): + """Test cutoff when PO is force invoiced before closed cutoff""" + cutoff = self.expense_cutoff + self._confirm_po_and_do_picking(2) + # Force invoiced before cutoff lines generated, lines should be deleted + self.po.force_invoiced = True + cutoff.get_lines() + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") + cutoff.state = "done" + # Remove Force invoiced, lines should be created + self.po.force_invoiced = False + self.assertEqual(len(cutoff.line_ids), 0, "no cutoff line should be generated") diff --git a/setup/account_cutoff_accrual_purchase_stock/odoo/addons/account_cutoff_accrual_purchase_stock b/setup/account_cutoff_accrual_purchase_stock/odoo/addons/account_cutoff_accrual_purchase_stock new file mode 120000 index 00000000000..23bc9126f9b --- /dev/null +++ b/setup/account_cutoff_accrual_purchase_stock/odoo/addons/account_cutoff_accrual_purchase_stock @@ -0,0 +1 @@ +../../../../account_cutoff_accrual_purchase_stock \ No newline at end of file diff --git a/setup/account_cutoff_accrual_purchase_stock/setup.py b/setup/account_cutoff_accrual_purchase_stock/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_accrual_purchase_stock/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 1a2e2670010e03b24df0f559347c7434cef04f0d Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Thu, 10 Oct 2024 17:46:27 +0200 Subject: [PATCH 7/7] [ADD] account_cutoff_accrual_order_stock_base --- .../README.rst | 86 ++++ .../__init__.py | 4 + .../__manifest__.py | 20 + .../i18n/fr.po | 35 ++ .../models/__init__.py | 4 + .../models/account_cutoff.py | 66 +++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 3 + .../static/description/index.html | 425 ++++++++++++++++++ .../views/account_cutoff_view.xml | 22 + .../account_cutoff_accrual_order_stock_base | 1 + .../setup.py | 6 + 12 files changed, 673 insertions(+) create mode 100644 account_cutoff_accrual_order_stock_base/README.rst create mode 100644 account_cutoff_accrual_order_stock_base/__init__.py create mode 100644 account_cutoff_accrual_order_stock_base/__manifest__.py create mode 100644 account_cutoff_accrual_order_stock_base/i18n/fr.po create mode 100644 account_cutoff_accrual_order_stock_base/models/__init__.py create mode 100644 account_cutoff_accrual_order_stock_base/models/account_cutoff.py create mode 100644 account_cutoff_accrual_order_stock_base/readme/CONTRIBUTORS.rst create mode 100644 account_cutoff_accrual_order_stock_base/readme/DESCRIPTION.rst create mode 100644 account_cutoff_accrual_order_stock_base/static/description/index.html create mode 100644 account_cutoff_accrual_order_stock_base/views/account_cutoff_view.xml create mode 120000 setup/account_cutoff_accrual_order_stock_base/odoo/addons/account_cutoff_accrual_order_stock_base create mode 100644 setup/account_cutoff_accrual_order_stock_base/setup.py diff --git a/account_cutoff_accrual_order_stock_base/README.rst b/account_cutoff_accrual_order_stock_base/README.rst new file mode 100644 index 00000000000..71deb59f739 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/README.rst @@ -0,0 +1,86 @@ +======================================== +Account Cut-off Accrual Order Stock Base +======================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:791c43ae54bd1847ebd5e0f75ad43368499c624e2ec8f67b58ac0e33b1a4a60c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--closing-lightgray.png?logo=github + :target: https://github.com/OCA/account-closing/tree/16.0/account_cutoff_accrual_order_stock_base + :alt: OCA/account-closing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-closing-16-0/account-closing-16-0-account_cutoff_accrual_order_stock_base + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-closing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of account_cutoff_base +to define prepaid accounts for prepaid goods in the scope of accrued revenue +and expense. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Jacques-Etienne Baudoux (BCIM) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-jbaudoux| image:: https://github.com/jbaudoux.png?size=40px + :target: https://github.com/jbaudoux + :alt: jbaudoux + +Current `maintainer `__: + +|maintainer-jbaudoux| + +This module is part of the `OCA/account-closing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_cutoff_accrual_order_stock_base/__init__.py b/account_cutoff_accrual_order_stock_base/__init__.py new file mode 100644 index 00000000000..86337901b11 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/account_cutoff_accrual_order_stock_base/__manifest__.py b/account_cutoff_accrual_order_stock_base/__manifest__.py new file mode 100644 index 00000000000..f85f684afd9 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/__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 Order Stock Base", + "version": "16.0.1.0.0", + "category": "Accounting & Finance", + "license": "AGPL-3", + "summary": "Accrued Order Stock Base", + "author": "BCIM, Odoo Community Association (OCA)", + "maintainers": ["jbaudoux"], + "website": "https://github.com/OCA/account-closing", + "depends": ["account_cutoff_base", "stock"], + "data": [ + "views/account_cutoff_view.xml", + ], + "installable": True, + "application": False, +} diff --git a/account_cutoff_accrual_order_stock_base/i18n/fr.po b/account_cutoff_accrual_order_stock_base/i18n/fr.po new file mode 100644 index 00000000000..2b94d42b94f --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/i18n/fr.po @@ -0,0 +1,35 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_cutoff_accrual_order_stock_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-26 12:01+0000\n" +"PO-Revision-Date: 2023-10-26 12:01+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_cutoff_accrual_order_stock_base +#: model:ir.model,name:account_cutoff_accrual_order_stock_base.model_account_cutoff +msgid "Account Cut-off" +msgstr "Provision" + +#. module: account_cutoff_accrual_order_stock_base +#: model:ir.model.fields,help:account_cutoff_accrual_order_stock_base.field_account_cutoff__cutoff_account_prepaid_stock_id +msgid "" +"Account for accrual of prepaid stock expenses. For instance, goods invoiced " +"and not yet received." +msgstr "" +"Compte pour les provisions sur charges de type marchandise constatées " +"d'avance. Par exemple, les marchandises facturées et pas encore reçues." + +#. module: account_cutoff_accrual_order_stock_base +#: model:ir.model.fields,field_description:account_cutoff_accrual_order_stock_base.field_account_cutoff__cutoff_account_prepaid_stock_id +msgid "Cut-off Prepaid Stock Account" +msgstr "Compte de provision pour marchandise prépayée" diff --git a/account_cutoff_accrual_order_stock_base/models/__init__.py b/account_cutoff_accrual_order_stock_base/models/__init__.py new file mode 100644 index 00000000000..4ef42780528 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import account_cutoff diff --git a/account_cutoff_accrual_order_stock_base/models/account_cutoff.py b/account_cutoff_accrual_order_stock_base/models/account_cutoff.py new file mode 100644 index 00000000000..d0873e5a591 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/models/account_cutoff.py @@ -0,0 +1,66 @@ +# Copyright 2018 Jacques-Etienne Baudoux (BCIM) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import Command, api, fields, models +from odoo.tools import float_is_zero + + +class AccountCutoff(models.Model): + _inherit = "account.cutoff" + + cutoff_account_prepaid_stock_id = fields.Many2one( + comodel_name="account.account", + string="Cut-off Prepaid Stock Account", + domain="[('deprecated', '=', False)]", + states={"done": [("readonly", True)]}, + check_company=True, + tracking=True, + default=lambda self: self._default_cutoff_account_prepaid_stock_id(), + help="Account for accrual of prepaid stock expenses. " + "For instance, goods invoiced and not yet received.", + ) + + @api.model + def _default_cutoff_account_prepaid_stock_id(self): + cutoff_type = self.env.context.get("default_cutoff_type") + company = self.env.company + if cutoff_type == "accrued_expense": + account_id = company.default_prepaid_expense_account_id.id or False + elif cutoff_type == "accrued_revenue": + account_id = company.default_prepaid_revenue_account_id.id or False + else: + account_id = False + return account_id + + def _prepare_counterpart_moves( + self, to_provision, amount_total_pos, amount_total_neg + ): + if not self.cutoff_account_prepaid_stock_id: + return super()._prepare_counterpart_moves( + to_provision, amount_total_pos, amount_total_neg + ) + if self.cutoff_type == "accrued_revenue": + prepaid_amount = amount_total_neg + amount = amount_total_pos + elif self.cutoff_type == "accrued_expense": + prepaid_amount = amount_total_pos + amount = amount_total_neg + else: + prepaid_amount = 0 + amount = 0 + company_currency = self.company_id.currency_id + cur_rprec = company_currency.rounding + movelines_to_create = super()._prepare_counterpart_moves( + to_provision, 0, amount + ) + if not float_is_zero(prepaid_amount, precision_rounding=cur_rprec): + movelines_to_create.append( + Command.create( + { + "account_id": self.cutoff_account_prepaid_stock_id.id, + "debit": prepaid_amount, + "credit": 0, + }, + ) + ) + return movelines_to_create diff --git a/account_cutoff_accrual_order_stock_base/readme/CONTRIBUTORS.rst b/account_cutoff_accrual_order_stock_base/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..3c6c5c696a8 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Jacques-Etienne Baudoux (BCIM) diff --git a/account_cutoff_accrual_order_stock_base/readme/DESCRIPTION.rst b/account_cutoff_accrual_order_stock_base/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..cd74bf5f06c --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module extends the functionality of account_cutoff_base +to define prepaid accounts for prepaid goods in the scope of accrued revenue +and expense. diff --git a/account_cutoff_accrual_order_stock_base/static/description/index.html b/account_cutoff_accrual_order_stock_base/static/description/index.html new file mode 100644 index 00000000000..0f97313e739 --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/static/description/index.html @@ -0,0 +1,425 @@ + + + + + + +Account Cut-off Accrual Order Stock Base + + + +
+

Account Cut-off Accrual Order Stock Base

+ + +

Beta License: AGPL-3 OCA/account-closing Translate me on Weblate Try me on Runboat

+

This module extends the functionality of account_cutoff_base +to define prepaid accounts for prepaid goods in the scope of accrued revenue +and expense.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

jbaudoux

+

This module is part of the OCA/account-closing project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_cutoff_accrual_order_stock_base/views/account_cutoff_view.xml b/account_cutoff_accrual_order_stock_base/views/account_cutoff_view.xml new file mode 100644 index 00000000000..871aae3075e --- /dev/null +++ b/account_cutoff_accrual_order_stock_base/views/account_cutoff_view.xml @@ -0,0 +1,22 @@ + + + + + + account.cutoff + + + + + + + + + diff --git a/setup/account_cutoff_accrual_order_stock_base/odoo/addons/account_cutoff_accrual_order_stock_base b/setup/account_cutoff_accrual_order_stock_base/odoo/addons/account_cutoff_accrual_order_stock_base new file mode 120000 index 00000000000..07d7bed1df6 --- /dev/null +++ b/setup/account_cutoff_accrual_order_stock_base/odoo/addons/account_cutoff_accrual_order_stock_base @@ -0,0 +1 @@ +../../../../account_cutoff_accrual_order_stock_base \ No newline at end of file diff --git a/setup/account_cutoff_accrual_order_stock_base/setup.py b/setup/account_cutoff_accrual_order_stock_base/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_accrual_order_stock_base/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)