Skip to content

Commit

Permalink
[ADD] account_cutoff_accrual_sale_stock
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaudoux committed Nov 8, 2023
1 parent 44ec43b commit 645c9e6
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 0 deletions.
Empty file.
4 changes: 4 additions & 0 deletions account_cutoff_accrual_sale_stock/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import models
21 changes: 21 additions & 0 deletions account_cutoff_accrual_sale_stock/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# 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,
}
4 changes: 4 additions & 0 deletions account_cutoff_accrual_sale_stock/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import sale_order_line
56 changes: 56 additions & 0 deletions account_cutoff_accrual_sale_stock/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2018 Jacques-Etienne Baudoux (BCIM sprl) <[email protected]>
# 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
moves_after = self.env["stock.move"].search(
[
("state", "=", "done"),
("date", ">=", cutoff_nextday),
("sale_line_id", "!=", False),
],
order="id",
)
sale_ids = set(moves_after.sale_line_id.order_id.ids)
sales = self.env["sale.order"].browse(sale_ids)
lines |= sales.order_line
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_quantity(self, cutoff):
self.ensure_one()
delivered_qty = super()._get_cutoff_accrual_delivered_quantity(cutoff)
# The quantity delivered on the SO line must be deducted from all
# moves done after the cutoff date.
cutoff_nextday = cutoff._nextday_start_dt()
moves_after = self.order_id.procurement_group_id.stock_move_ids.filtered(
lambda r: r.state == "done" and r.date >= cutoff_nextday
)
for move in moves_after:
if move.product_uom != self.product_uom:
delivered_qty -= move.product_uom._compute_quantity(
move.product_uom_qty, self.product_uom
)
else:
delivered_qty -= move.product_uom_qty
return delivered_qty
1 change: 1 addition & 0 deletions account_cutoff_accrual_sale_stock/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_cutoff_revenue
88 changes: 88 additions & 0 deletions account_cutoff_accrual_sale_stock/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# 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;")
# Create SO
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": 5,
"product_uom": p.uom_id.id,
"price_unit": 100,
"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,
}
)
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(),
}
)
)

def _confirm_so_and_do_picking(self, qty_done):
self.so.action_confirm()
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 for p in self.products],
"Delivered quantities are wrong after partial delivery",
)
177 changes: 177 additions & 0 deletions account_cutoff_accrual_sale_stock/tests/test_cutoff_revenue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from datetime import timedelta

from .common import TestAccountCutoffAccrualSaleCommon


class TestAccountCutoffAccrualSale(TestAccountCutoffAccrualSaleCommon):
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), 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_revenue_tax_line(self):
cutoff = self.revenue_cutoff
self._confirm_so_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_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), 2, "2 cutoff lines should be found")
for line in cutoff.line_ids:
self.assertEqual(
line.cutoff_amount, 100 * 2, "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), 2, "2 cutoff lines should be found")
for line in cutoff.line_ids:
self.assertEqual(
line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect"
)
# Validate invoice
self.so.invoice_ids.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, "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), 2, "2 cutoff lines should be found")
for line in cutoff.line_ids:
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
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), 2, "No cutoff lines should be found")
for line in cutoff.line_ids:
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
self.so._create_invoices(final=True)
# - invoice is in draft, no change to cutoff
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, "SO line cutoff amount incorrect"
)
# Validate invoice
self.so.invoice_ids.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 - the refund reset SO lines qty_invoiced
self._refund_invoice(self.so.invoice_ids)
self.assertEqual(len(cutoff.line_ids), 2, "2 cutoff lines should be found")
for line in cutoff.line_ids:
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
self.so._create_invoices(final=True)
# - 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, "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), 2, "2 cutoff lines should be found")
for line in cutoff.line_ids:
self.assertEqual(
line.cutoff_amount, 100 * 2, "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), 2, "2 cutoff lines should be found")
for line in cutoff.line_ids:
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
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()
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, 2 * 100, "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), 2, "no cutoff lines should be found")
for line in cutoff.line_ids:
self.assertEqual(
line.cutoff_amount, 100 * 2, "SO line cutoff amount incorrect"
)
6 changes: 6 additions & 0 deletions setup/account_cutoff_accrual_sale_stock/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

0 comments on commit 645c9e6

Please sign in to comment.