Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions purchase_ux/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,14 @@ def action_purchase_matching(self):
]
# Show POLs with something pending, with a different criterion per document type:
# * on a bill (in_invoice): ordered qty not billed yet (product_qty > qty_invoiced),
# received or not. qty_to_invoice cannot be used here because on products
# controlled on received quantities it is qty_received - qty_invoiced, so a
# received or not, or qty pending to bill (qty_to_invoice > 0) for an over receipt,
# fully ordered but not fully billed. qty_to_invoice cannot be used alone because on
# products controlled on received quantities it is qty_received - qty_invoiced, so a
# confirmed PO with no receipt yet gives 0 and the line would be hidden.
# * on a credit note (in_refund): lines left with a pending refund by a return,
# ie. billed more than received (qty_to_invoice < 0). product_qty cannot be used
# here because it does not drop with a return, so a fully billed line gives 0.
# POs with a forced invoice status are excluded: nothing left to bill on them.
all_pols = self.env["purchase.order.line"].search(
[
("partner_id", "in", commercial_partner.ids),
Expand All @@ -121,11 +123,14 @@ def action_purchase_matching(self):
uom_precision = self.env["decimal.precision"].precision_get("Product Unit of Measure")

def _pending(pol):
if pol.id in already_matched:
if pol.id in already_matched or pol.order_id.force_invoiced_status:
return False
if is_refund:
return float_compare(pol.qty_to_invoice, 0.0, precision_digits=uom_precision) < 0
return float_compare(pol.product_qty, pol.qty_invoiced, precision_digits=uom_precision) > 0
return (
float_compare(pol.product_qty, pol.qty_invoiced, precision_digits=uom_precision) > 0
or float_compare(pol.qty_to_invoice, 0.0, precision_digits=uom_precision) > 0
)

pending_pol_ids = all_pols.filtered(_pending).ids
domain = list(res.get("domain") or [])
Expand Down
20 changes: 20 additions & 0 deletions purchase_ux/models/purchase_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def _get_invoiced(self):
def button_set_invoiced(self):
if not self.env.user.has_group("base.group_system"):
group = self.env.ref("base.group_system").sudo()
<<<<<<< bed80522437e95ca8fdfb04e2f801b9beced261b
if group.privilege_id:
raise UserError(
_('Only users with "%s / %s" can Set Invoiced manually') % (group.privilege_id.name, group.name)
Expand All @@ -56,6 +57,25 @@ def button_set_invoiced(self):

self.write({"invoice_status": "invoiced"})
self.order_line.write({"qty_to_invoice": 0.0})
||||||| 44a1146a97c5cbcb5e7733eb02d6d7bf66bad909
raise UserError(
_('Only users with "%s / %s" can Set Invoiced manually') % (group.category_id.name, group.name)
)
# In purchases the invoice status is not calculated from the lines,
# so we step on it in the PO. Do not step on the qty_invoiced because
# it seems more neat to restore what happened

self.write({"invoice_status": "invoiced"})
self.order_line.write({"qty_to_invoice": 0.0})
=======
raise UserError(
_('Only users with "%s / %s" can Set Invoiced manually') % (group.category_id.name, group.name)
)
# force_invoiced_status is the only value that survives a recompute: both
# invoice_status and qty_to_invoice are stored computed fields, so stamping
# them was undone by the next recompute of the order or its lines.
self.write({"force_invoiced_status": "invoiced"})
>>>>>>> 869952226fa0e3a7a7ecdb217f1108ddbcf122e1
self.message_post(body=_("Manually setted as invoiced"))

def write(self, vals):
Expand Down
1 change: 1 addition & 0 deletions purchase_ux/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
# directory
##############################################################################

from . import test_purchase_matching
from . import test_purchase_order
108 changes: 108 additions & 0 deletions purchase_ux/tests/test_purchase_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
##############################################################################
# For copyright and license notices, see __manifest__.py file in module root
# directory
##############################################################################
from odoo import Command, fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged


@tagged("post_install", "-at_install")
class TestPurchaseMatching(AccountTestInvoicingCommon):
"""Lines offered by the purchase matching action, ordered/received/billed."""

@classmethod
def setUpClass(cls):
super().setUpClass()
# forcing the invoice status of a PO is restricted to settings managers
cls.env.user.groups_id |= cls.env.ref("base.group_system")
cls.vendor = cls.env["res.partner"].create({"name": "Test Vendor Matching"})
# a service controlled on received quantities lets us set qty_received by hand
cls.product = cls.env["product.product"].create(
{
"name": "Test Service On Received",
"type": "service",
"purchase_method": "receive",
}
)

def _line(self, ordered, received=0.0, forced=False):
purchase = self.env["purchase.order"].create(
{
"partner_id": self.vendor.id,
"order_line": [
Command.create({"product_id": self.product.id, "product_qty": ordered, "price_unit": 100.0})
],
}
)
purchase.button_confirm()
purchase.order_line.qty_received = received
purchase.force_invoiced_status = forced
return purchase.order_line

def _bill(self, line, quantity, move_type="in_invoice"):
self.env["account.move"].create(
{
"move_type": move_type,
"partner_id": self.vendor.id,
"invoice_date": fields.Date.today(),
"invoice_line_ids": [
Command.create(
{
"product_id": self.product.id,
"quantity": quantity,
"price_unit": 100.0,
"purchase_line_id": line.id,
"tax_ids": False,
}
)
],
}
).action_post()

def _offered(self, move_type="in_invoice"):
move = self.env["account.move"].create({"move_type": move_type, "partner_id": self.vendor.id})
action = move.action_purchase_matching()
self.env.flush_all() # the matching model is a SQL view read from the database
return self.env["purchase.bill.line.match"].search(action["domain"]).pol_id

def test_bill_not_received(self):
self.assertIn(self._line(100), self._offered())

def test_bill_partially_received(self):
self.assertIn(self._line(100, received=60), self._offered())

def test_bill_over_receipt(self):
line = self._line(40, received=41)
self._bill(line, 40)
self.assertIn(line, self._offered())

def test_bill_fully_billed(self):
line = self._line(100, received=100)
self._bill(line, 100)
self.assertNotIn(line, self._offered())

def test_bill_forced_invoiced_status(self):
self.assertNotIn(self._line(100, forced="invoiced"), self._offered())

def test_bill_set_invoiced_button(self):
"""The Set Invoiced button closes the order for billing, and it sticks."""
line = self._line(100, received=60)
line.order_id.button_set_invoiced()
self.assertEqual(line.order_id.force_invoiced_status, "invoiced")
self.assertEqual(line.order_id.invoice_status, "invoiced")
self.assertNotIn(line, self._offered())
line.qty_received = 80 # a later recompute must not bring it back
self.assertEqual(line.order_id.invoice_status, "invoiced")
self.assertNotIn(line, self._offered())

def test_refund_pending_from_return(self):
line = self._line(600, received=500)
self._bill(line, 600)
self.assertIn(line, self._offered(move_type="in_refund"))

def test_refund_already_credited(self):
line = self._line(600, received=500)
self._bill(line, 600)
self._bill(line, 100, move_type="in_refund")
self.assertNotIn(line, self._offered(move_type="in_refund"))
Loading