-
-
Notifications
You must be signed in to change notification settings - Fork 247
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ADD] account_cutoff_accrual_sale_stock
- Loading branch information
Showing
9 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). | ||
|
||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# 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)", | ||
"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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). | ||
|
||
from . import sale_order_line |
56 changes: 56 additions & 0 deletions
56
account_cutoff_accrual_sale_stock/models/sale_order_line.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# Copyright 2018 Jacques-Etienne Baudoux (BCIM sprl) <je@bcim.be> | ||
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import test_cutoff_revenue |
260 changes: 260 additions & 0 deletions
260
account_cutoff_accrual_sale_stock/tests/test_cutoff_revenue.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
# Copyright 2018 Jacques-Etienne Baudoux (BCIM) <je@bcim.be> | ||
|
||
from datetime import timedelta | ||
|
||
from odoo import Command, fields | ||
|
||
from odoo.addons.account_cutoff_accrual_order_base.tests.common import ( | ||
TestAccountCutoffCutoffPickingCommon, | ||
) | ||
|
||
|
||
class AccountCutoffCutoffRevenueCommon(TestAccountCutoffCutoffPickingCommon): | ||
@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", | ||
) | ||
|
||
|
||
class TestAccountCutoffCutoffRevenue(AccountCutoffCutoffRevenueCommon): | ||
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" | ||
) |
1 change: 1 addition & 0 deletions
1
setup/account_cutoff_accrual_sale_stock/odoo/addons/account_cutoff_accrual_sale_stock
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../account_cutoff_accrual_sale_stock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |