Merge PR #1953 into 17.0

Signed-off-by JordiBForgeFlow
This commit is contained in:
OCA-git-bot
2024-10-16 07:28:59 +00:00
21 changed files with 1602 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
=====================
Purchase Unreconciled
=====================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8134013d5d37fe8931266d6f916bbc160f48212ac4a9faa43b4efe39d03e6b4c
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |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--financial--tools-lightgray.png?logo=github
:target: https://github.com/OCA/account-financial-tools/tree/17.0/purchase_unreconciled
:alt: OCA/account-financial-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-financial-tools-17-0/account-financial-tools-17-0-purchase_unreconciled
: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-financial-tools&target_branch=17.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds a new fields "Unreconciled" on Purchase Orders, that
allows to find PO's with unreconciled journal items related.
This module allows to reconcile those PO in a single click. In
accounting settings users will be able to set up a specific account for
write-off.
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Usage
=====
Accountants will be able to find a filters in Purchase Orders that shows
outstanding balances in interim accounts. Also there is a link in the PO
to those outstanding journal items.
Locking the PO will automatically reconcile the outstanding balance for
the stock iterim accounts.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/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 <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20purchase_unreconciled%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* ForgeFlow S.L.
Contributors
------------
- ForgeFlow S.L. <contact@forgeflow.com>
- Lois Rilo <lois.rilo@forgeflow.com>
- Aaron Henriquez <ahenriquez@forgeflow.com>
- Miquel Raich <miquel.raich@forgeflow.com>
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-AaronHForgeFlow| image:: https://github.com/AaronHForgeFlow.png?size=40px
:target: https://github.com/AaronHForgeFlow
:alt: AaronHForgeFlow
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-AaronHForgeFlow|
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/17.0/purchase_unreconciled>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,2 @@
from . import models
from . import wizards

View File

@@ -0,0 +1,21 @@
# Copyright 2019-22 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Purchase Unreconciled",
"version": "17.0.1.0.0",
"author": "ForgeFlow S.L., Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-financial-tools",
"category": "Purchases",
"depends": ["account_move_line_purchase_info", "purchase_stock"],
"data": [
"security/ir.model.access.csv",
"views/purchase_order_view.xml",
"views/res_config_settings_view.xml",
"wizards/purchase_unreconciled_exceeded_view.xml",
],
"license": "AGPL-3",
"installable": True,
"development_status": "Alpha",
"maintainers": ["AaronHForgeFlow"],
}

View File

@@ -0,0 +1,4 @@
from . import purchase_order
from . import company
from . import res_config_settings
from . import account_move_line

View File

@@ -0,0 +1,90 @@
# Copyright 2019-21 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import datetime
from odoo import _, models
from odoo.exceptions import ValidationError
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
def _get_writeoff_amounts(self):
precision = self.env["decimal.precision"].precision_get("Account")
writeoff_amount = round(
sum(line["amount_residual"] for line in self), precision
)
writeoff_amount_curr = round(
sum(line["amount_residual_currency"] for line in self), precision
)
if writeoff_amount_curr and not writeoff_amount:
# Data inconsistency, do not create the write-off
return (0.0, 0.0, True)
first_currency = self[0]["currency_id"]
if all([line["currency_id"] == first_currency for line in self]):
same_curr = True
else:
same_curr = False
return (
writeoff_amount,
writeoff_amount_curr,
same_curr,
)
def _create_writeoff(self, writeoff_vals):
(
amount_writeoff,
amount_writeoff_curr,
same_curr,
) = self._get_writeoff_amounts()
if not amount_writeoff:
return self.env["account.move.line"]
partners = self.mapped("partner_id")
move_date = writeoff_vals.get("date", datetime.now())
write_off_vals = {
"name": _("Automatic writeoff"),
"amount_currency": same_curr and amount_writeoff_curr or amount_writeoff,
"debit": amount_writeoff > 0.0 and amount_writeoff or 0.0,
"credit": amount_writeoff < 0.0 and -amount_writeoff or 0.0,
"partner_id": len(partners) == 1 and partners.id or False,
"account_id": writeoff_vals["account_id"],
"date": move_date,
"journal_id": writeoff_vals["journal_id"],
"currency_id": writeoff_vals.get("currency_id", False),
"product_id": writeoff_vals["product_id"],
"purchase_order_id": writeoff_vals["purchase_order_id"],
"purchase_line_id": writeoff_vals["purchase_line_id"],
}
counterpart_account = self.mapped("account_id")
if len(counterpart_account) != 1:
raise ValidationError(_("Cannot write-off more than one account"))
counter_part = write_off_vals.copy()
counter_part["debit"] = write_off_vals["credit"]
counter_part["credit"] = write_off_vals["debit"]
counter_part["amount_currency"] = -write_off_vals["amount_currency"]
counter_part["account_id"] = (counterpart_account.id,)
move = self.env["account.move"].create(
{
"date": move_date,
"journal_id": writeoff_vals["journal_id"],
"currency_id": writeoff_vals.get("currency_id", False),
"line_ids": [(0, 0, write_off_vals), (0, 0, counter_part)],
}
)
if writeoff_vals.get("purchase_order_id", False):
# done this way because purchase_order_id is a related field and will
# not being assign on create. Cannot assign purchase_line_id because
# it is a generic write-off for the whole PO
self.env.cr.execute(
"""UPDATE account_move_line SET purchase_order_id = %s
WHERE id in %s
""",
(writeoff_vals["purchase_order_id"], tuple(move.line_ids.ids)),
)
move.action_post()
return move.line_ids.filtered(
lambda line: line.account_id.id == counterpart_account.id
)

View File

@@ -0,0 +1,27 @@
# Copyright 2019-21 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
purchase_reconcile_account_id = fields.Many2one(
"account.account",
domain=lambda self: [("deprecated", "=", False)],
string="Write-Off Account On Purchases",
ondelete="restrict",
copy=False,
help="Write-off account to reconcile Unreconciled Purchase Orders",
)
purchase_reconcile_journal_id = fields.Many2one(
"account.journal", string="WriteOff Journal for Purchases"
)
purchase_lock_auto_reconcile = fields.Boolean()
purchase_reconcile_tolerance = fields.Float(
string="Purchase Reconcile Tolerance (%)",
default=0.0,
help="Percentage of tolerance of residual amount vs total amount of the "
"Purchase Order. Leave zero to accept all discrepancies",
)

View File

@@ -0,0 +1,276 @@
# Copyright 2019-21 ForgeFlow S.L..
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, api, exceptions, fields, models
from odoo.osv import expression
from odoo.tools import float_is_zero
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
unreconciled = fields.Boolean(
compute="_compute_unreconciled",
search="_search_unreconciled",
help="Indicates that a Purchase Order has related Journal items not "
"reconciled.\nNote that if it is false it can be either that "
"everything is reconciled or that the related accounts do not "
"allow reconciliation",
)
amount_unreconciled = fields.Float(compute="_compute_unreconciled")
def _get_account_domain(self):
included_accounts = (
(
self.env["product.category"]
.with_company(self.company_id.id)
.search([("property_valuation", "=", "real_time")])
)
.mapped("property_stock_account_input_categ_id")
.ids
)
return [("account_id", "in", included_accounts)]
@api.model
def _get_purchase_unreconciled_base_domain(self):
unreconciled_domain = [
("account_id.reconcile", "=", True),
("move_id.state", "=", "posted"),
("company_id", "in", self.env.companies.ids),
# same condition than Odoo Unreconciled filter
("amount_residual", "!=", 0.0),
("balance", "!=", 0.0),
]
return unreconciled_domain
def _compute_unreconciled(self):
acc_item = self.env["account.move.line"]
for rec in self:
domain = rec.with_company(
rec.company_id
)._get_purchase_unreconciled_base_domain()
domain_account = rec._get_account_domain()
unreconciled_domain = expression.AND([domain, domain_account])
unreconciled_domain = expression.AND(
[unreconciled_domain, [("purchase_order_id", "=", rec.id)]]
)
unreconciled_items = acc_item.search(unreconciled_domain)
rec.unreconciled = len(unreconciled_items) > 0
rec.amount_unreconciled = sum(unreconciled_items.mapped("amount_residual"))
def _search_unreconciled(self, operator, value):
if operator not in ("=", "!=") or not isinstance(value, bool):
raise ValueError(_("Unsupported search operator"))
acc_item = self.env["account.move.line"]
domain = self._get_purchase_unreconciled_base_domain()
domain = expression.AND([domain, [("purchase_order_id", "!=", False)]])
domain_account = self._get_account_domain()
domain = expression.AND([domain_account, domain])
acc_items = acc_item.search(domain)
unreconciled_pos_ids = acc_items.mapped("purchase_order_id").ids
if value:
return [("id", "in", unreconciled_pos_ids)]
else:
return [("id", "not in", unreconciled_pos_ids)]
def action_view_unreconciled(self):
self.ensure_one()
acc_item = self.env["account.move.line"]
domain = self.with_company(
self.company_id.id
)._get_purchase_unreconciled_base_domain()
domain_account = self._get_account_domain()
unreconciled_domain = expression.AND([domain, domain_account])
unreconciled_domain = expression.AND(
[unreconciled_domain, [("purchase_order_id", "=", self.id)]]
)
unreconciled_domain.remove(("amount_residual", "!=", 0.0))
unreconciled_domain.remove("&")
unreconciled_items = acc_item.search(unreconciled_domain)
action = self.env.ref("account.action_account_moves_all")
action_dict = action.read()[0]
action_dict["domain"] = [("id", "in", unreconciled_items.ids)]
return action_dict
def action_reconcile(self):
if (
not self.company_id.purchase_reconcile_account_id
or not self.company_id.purchase_reconcile_journal_id
):
raise exceptions.ValidationError(
_(
"The write-off account and journal for purchases is missing. An "
"accountant must fill that information"
)
)
self.ensure_one()
res = {}
domain = self._get_purchase_unreconciled_base_domain()
domain_account = self._get_account_domain()
unreconciled_domain = expression.AND([domain, domain_account])
unreconciled_domain = expression.AND(
[domain, [("purchase_order_id", "=", self.id)]]
)
unreconciled_domain = expression.AND(
[unreconciled_domain, [("company_id", "=", self.company_id.id)]]
)
writeoff_to_reconcile = self.env["account.move.line"]
all_writeoffs = self.env["account.move.line"]
reconciling_groups = self.env["account.move.line"].read_group(
domain=unreconciled_domain,
fields=["account_id", "product_id", "purchase_line_id"],
groupby=["account_id", "product_id", "purchase_line_id"],
lazy=False,
)
unreconciled_items = self.env["account.move.line"].search(unreconciled_domain)
for group in reconciling_groups:
account_id = group["account_id"][0]
product_id = group["product_id"][0] if group["product_id"] else False
purchase_line_id = (
group["purchase_line_id"][0] if group["purchase_line_id"] else False
)
unreconciled_items_group = unreconciled_items.filtered(
lambda line, account_id=account_id, product_id=product_id: (
line.account_id.id == account_id
and line.product_id.id == product_id
)
)
# Check which type of force reconciling we are doing:
# - Force reconciling amount_residual
# - Force reconciling amount_residual_currency
amount_residual_currency_reconcile = any(
unreconciled_items_group.filtered(
lambda item_group,
account_id=account_id: item_group.amount_residual_currency != 0.0
and item_group.account_id.id == account_id
)
)
if amount_residual_currency_reconcile:
residual_field = "amount_residual_currency"
else:
residual_field = "amount_residual"
if float_is_zero(
sum(unreconciled_items_group.mapped(residual_field)),
precision_rounding=self.company_id.currency_id.rounding,
):
moves_to_reconcile = unreconciled_items_group
else:
writeoff_vals = self._get_purchase_writeoff_vals(
unreconciled_items_group, purchase_line_id, product_id
)
writeoff_to_reconcile = unreconciled_items_group._create_writeoff(
writeoff_vals
)
all_writeoffs |= writeoff_to_reconcile
# add writeoff line to reconcile algorithm and finish the reconciliation
moves_to_reconcile = unreconciled_items_group | writeoff_to_reconcile
# Check if reconciliation is total or needs an exchange rate entry to be
# created
if moves_to_reconcile:
moves_to_reconcile.filtered(
lambda move: not move.reconciled
).reconcile()
reconciled_ids = unreconciled_items | all_writeoffs
res = {
"name": _("Reconciled journal items"),
"type": "ir.actions.act_window",
"view_type": "form",
"view_mode": "tree,form",
"res_model": "account.move.line",
"domain": [("id", "in", reconciled_ids.ids)],
}
if self.env.context.get("bypass_unreconciled", False):
# When calling the method from the wizard, lock after reconciling
self.button_done()
return res
def _get_purchase_writeoff_vals(self, amls, purchase_line_id, product_id):
writeoff_date = self.env.context.get("writeoff_date", False)
aml_date = max(amls.mapped("move_id.date"))
res = {
"account_id": self.company_id.purchase_reconcile_account_id.id,
"journal_id": self.company_id.purchase_reconcile_journal_id.id,
"purchase_order_id": self.id,
"purchase_line_id": purchase_line_id or False,
"product_id": product_id,
"currency_id": self.currency_id.id or self.env.company.currency_id.id,
"date": aml_date,
}
# hook for custom date:
if writeoff_date:
res.update({"date": writeoff_date})
return res
def reconcile_criteria(self):
"""Gets the criteria where POs are locked or not, by default uses the company
configuration"""
self.ensure_one()
return self.unreconciled and self.company_id.purchase_lock_auto_reconcile
def button_done(self):
for rec in self:
criteria = rec.reconcile_criteria()
if criteria:
if rec.unreconciled:
exception_msg = rec.unreconciled_exception_msg()
if exception_msg:
res = rec.purchase_unreconciled_exception(exception_msg)
return res
else:
rec.action_reconcile()
return super(PurchaseOrder, rec).button_done()
else:
return super(PurchaseOrder, rec).button_done()
else:
return super(PurchaseOrder, rec).button_done()
def purchase_unreconciled_exception(self, exception_msg=None):
"""This mean to be run when the SO cannot be reconciled because it is over
tolerance"""
self.ensure_one()
if exception_msg:
return (
self.env["purchase.unreconciled.exceeded.wiz"]
.create(
{
"exception_msg": exception_msg,
"purchase_id": self.id,
"origin_reference": "{},{}".format("purchase.order", self.id),
"continue_method": "action_reconcile",
}
)
.action_show()
)
def unreconciled_exception_msg(self):
self.ensure_one()
exception_msg = ""
amount_total = self.amount_total
if self.currency_id and self.company_id.currency_id != self.currency_id:
amount_total = self.currency_id._convert(
amount_total,
self.company_id.currency_id,
self.company_id,
fields.Date.today(),
)
if (
self.company_id.purchase_reconcile_tolerance
and amount_total
and abs(self.amount_unreconciled / amount_total)
>= self.company_id.purchase_reconcile_tolerance / 100.0
):
params = {
"amount_unreconciled": self.amount_unreconciled,
"amount_allowed": self.amount_total
* self.company_id.purchase_reconcile_tolerance
/ 100.0,
}
exception_msg = (
_(
"Finance Warning: \nUnreconciled amount is too high. Total "
"unreconciled amount: %(amount_unreconciled)s Maximum unreconciled"
" amount accepted: %(amount_allowed)s "
)
% params
)
return exception_msg

View File

@@ -0,0 +1,21 @@
# Copyright 2019-21 ForgeFlow S.L..
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
purchase_reconcile_account_id = fields.Many2one(
related="company_id.purchase_reconcile_account_id", readonly=False
)
purchase_reconcile_journal_id = fields.Many2one(
related="company_id.purchase_reconcile_journal_id", readonly=False
)
purchase_lock_auto_reconcile = fields.Boolean(
related="company_id.purchase_lock_auto_reconcile", readonly=False
)
purchase_reconcile_tolerance = fields.Float(
related="company_id.purchase_reconcile_tolerance", readonly=False
)

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,4 @@
- ForgeFlow S.L. \<<contact@forgeflow.com>\>
- Lois Rilo \<<lois.rilo@forgeflow.com>\>
- Aaron Henriquez \<<ahenriquez@forgeflow.com>\>
- Miquel Raich \<<miquel.raich@forgeflow.com>\>

View File

@@ -0,0 +1,6 @@
This module adds a new fields "Unreconciled" on Purchase Orders, that
allows to find PO's with unreconciled journal items related.
This module allows to reconcile those PO in a single click. In
accounting settings users will be able to set up a specific account for
write-off.

View File

@@ -0,0 +1,6 @@
Accountants will be able to find a filters in Purchase Orders that shows
outstanding balances in interim accounts. Also there is a link in the PO
to those outstanding journal items.
Locking the PO will automatically reconcile the outstanding balance for
the stock iterim accounts.

View File

@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
purchase.unreconciled.exceeded.wiz,purchase.unreconciled.exceeded.wiz,purchase_unreconciled.model_purchase_unreconciled_exceeded_wiz,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 purchase.unreconciled.exceeded.wiz purchase.unreconciled.exceeded.wiz purchase_unreconciled.model_purchase_unreconciled_exceeded_wiz base.group_user 1 1 1 1

View File

@@ -0,0 +1,449 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Purchase Unreconciled</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="purchase-unreconciled">
<h1 class="title">Purchase Unreconciled</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:8134013d5d37fe8931266d6f916bbc160f48212ac4a9faa43b4efe39d03e6b4c
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-financial-tools/tree/17.0/purchase_unreconciled"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-financial-tools-17-0/account-financial-tools-17-0-purchase_unreconciled"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds a new fields “Unreconciled” on Purchase Orders, that
allows to find POs with unreconciled journal items related.</p>
<p>This module allows to reconcile those PO in a single click. In
accounting settings users will be able to set up a specific account for
write-off.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>Accountants will be able to find a filters in Purchase Orders that shows
outstanding balances in interim accounts. Also there is a link in the PO
to those outstanding journal items.</p>
<p>Locking the PO will automatically reconcile the outstanding balance for
the stock iterim accounts.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-financial-tools/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/account-financial-tools/issues/new?body=module:%20purchase_unreconciled%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>ForgeFlow S.L.</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>ForgeFlow S.L. &lt;<a class="reference external" href="mailto:contact&#64;forgeflow.com">contact&#64;forgeflow.com</a>&gt;<ul>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
<li>Aaron Henriquez &lt;<a class="reference external" href="mailto:ahenriquez&#64;forgeflow.com">ahenriquez&#64;forgeflow.com</a>&gt;</li>
<li>Miquel Raich &lt;<a class="reference external" href="mailto:miquel.raich&#64;forgeflow.com">miquel.raich&#64;forgeflow.com</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/AaronHForgeFlow"><img alt="AaronHForgeFlow" src="https://github.com/AaronHForgeFlow.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/17.0/purchase_unreconciled">OCA/account-financial-tools</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_purchase_unreconciled

View File

@@ -0,0 +1,427 @@
# Copyright 2019-21 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import datetime
from odoo import exceptions, fields
from odoo.tests.common import Form, SingleTransactionCase
class TestPurchaseUnreconciled(SingleTransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.po_obj = cls.env["purchase.order"]
cls.product_obj = cls.env["product.product"]
cls.category_obj = cls.env["product.category"]
cls.partner_obj = cls.env["res.partner"]
cls.acc_obj = cls.env["account.account"]
cls.account_move_obj = cls.env["account.move"]
cls.company = cls.env.ref("base.main_company")
cls.company.anglo_saxon_accounting = True
expense_type = "expense"
equity_type = "equity"
asset_type = "asset_current"
# Create partner:
cls.partner = cls.partner_obj.create({"name": "Test Vendor"})
# Create product that uses a reconcilable stock input account.
cls.account = cls.acc_obj.create(
{
"name": "Test stock input account",
"code": 9999,
"account_type": asset_type,
"reconcile": True,
"company_id": cls.company.id,
}
)
cls.writeoff_acc = cls.acc_obj.create(
{
"name": "Write-offf account",
"code": 8888,
"account_type": expense_type,
"reconcile": True,
"company_id": cls.company.id,
}
)
cls.stock_journal = cls.env["account.journal"].create(
{"name": "Stock Journal", "code": "STJTEST", "type": "general"}
)
# Create account for Goods Received Not Invoiced
name = "Goods Received Not Invoiced"
code = "grni"
acc_type = equity_type
cls.account_grni = cls._create_account(
acc_type, name, code, cls.company, reconcile=True
)
# Create account for Cost of Goods Sold
name = "Cost of Goods Sold"
code = "cogs"
acc_type = expense_type
cls.account_cogs = cls._create_account(acc_type, name, code, cls.company)
# Create account for Goods Delivered Not Invoiced
name = "Goods Delivered Not Invoiced"
code = "gdni"
acc_type = expense_type
cls.account_gdni = cls._create_account(
acc_type, name, code, cls.company, reconcile=True
)
# Create account for Inventory
name = "Inventory"
code = "inventory"
acc_type = asset_type
cls.account_inventory = cls._create_account(acc_type, name, code, cls.company)
cls.product_categ = cls.category_obj.create(
{
"name": "Test Category",
"property_cost_method": "standard",
"property_stock_valuation_account_id": cls.account_inventory.id,
"property_stock_account_input_categ_id": cls.account_grni.id,
"property_account_expense_categ_id": cls.account_cogs.id,
"property_stock_account_output_categ_id": cls.account_gdni.id,
"property_valuation": "real_time",
"property_stock_journal": cls.stock_journal.id,
}
)
cls.product_to_reconcile = cls.product_obj.create(
{
"name": "Purchased Product (To reconcile)",
"type": "product",
"standard_price": 100.0,
"categ_id": cls.product_categ.id,
}
)
cls.product_to_reconcile2 = cls.product_obj.create(
{
"name": "Purchased Product 2 (To reconcile)",
"type": "product",
"standard_price": 100.0,
"categ_id": cls.product_categ.id,
}
)
# Create PO's:
cls.po = cls.po_obj.create(
{
"partner_id": cls.partner.id,
"order_line": [
(
0,
0,
{
"product_id": cls.product_to_reconcile.id,
"name": cls.product_to_reconcile.name,
"product_qty": 5.0,
"price_unit": 100.0,
"product_uom": cls.product_to_reconcile.uom_id.id,
"date_planned": fields.Datetime.now(),
},
)
],
}
)
cls.po_2 = cls.po_obj.create(
{
"partner_id": cls.partner.id,
"order_line": [
(
0,
0,
{
"product_id": cls.product_to_reconcile.id,
"name": cls.product_to_reconcile.name,
"product_qty": 5.0,
"price_unit": 100.0,
"product_uom": cls.product_to_reconcile.uom_id.id,
"date_planned": fields.Datetime.now(),
},
)
],
}
)
# company settings for automated valuation
cls.company.purchase_lock_auto_reconcile = True
cls.company.purchase_reconcile_account_id = cls.writeoff_acc
cls.company.purchase_reconcile_journal_id = cls.stock_journal
@classmethod
def _create_account(cls, acc_type, name, code, company, reconcile=False):
"""Create an account."""
account = cls.acc_obj.create(
{
"name": name,
"code": code,
"account_type": acc_type,
"company_id": company.id,
"reconcile": reconcile,
}
)
return account
def _create_delivery(
self,
product,
qty,
):
return self.env["stock.picking"].create(
{
"name": self.product_to_reconcile.name,
"partner_id": self.partner.id,
"picking_type_id": self.env.ref("stock.picking_type_out").id,
"location_id": self.env.ref("stock.stock_location_stock").id,
"location_dest_id": self.env.ref("stock.stock_location_customers").id,
"move_ids": [
(
0,
0,
{
"name": self.product_to_reconcile.name,
"product_id": self.product_to_reconcile.id,
"product_uom": self.product_to_reconcile.uom_id.id,
"product_uom_qty": qty,
"location_id": self.env.ref(
"stock.stock_location_stock"
).id,
"location_dest_id": self.env.ref(
"stock.stock_location_customers"
).id,
"procure_method": "make_to_stock",
},
)
],
}
)
def _do_picking(self, picking, date):
"""Do picking with only one move on the given date."""
picking.action_confirm()
picking.action_assign()
for move in picking.move_ids:
move.quantity = move.product_uom_qty
move.date = date
picking.button_validate()
def test_01_nothing_to_reconcile(self):
po = self.po
self.assertEqual(po.state, "draft")
po.button_confirm()
self._do_picking(po.picking_ids, fields.Datetime.now())
self.assertTrue(po.unreconciled)
# Invoice created and validated:
po.action_create_invoice()
invoice_ids = po.invoice_ids.filtered(lambda i: i.move_type == "in_invoice")
invoice_ids.invoice_date = datetime.now()
invoice_ids.action_post()
self.assertEqual(po.state, "purchase")
# odoo does it automatically
po._compute_unreconciled()
self.assertFalse(po.unreconciled)
def test_03_search_unreconciled(self):
"""Test searching unreconciled PO's."""
po = self.po_2
po.button_confirm()
self._do_picking(po.picking_ids, fields.Datetime.now())
res = self.po_obj.search([("unreconciled", "=", True)])
po._compute_unreconciled()
self.assertIn(po, res)
self.assertNotIn(self.po, res)
# Test value error:
with self.assertRaises(ValueError):
self.po_obj.search([("unreconciled", "=", "true")])
def test_04_action_reconcile(self):
"""Test reconcile."""
# Invoice created and validated:
po = self.po_2
self.assertTrue(po.unreconciled)
po.action_create_invoice()
invoice_form = Form(
po.invoice_ids.filtered(lambda i: i.move_type == "in_invoice")[0]
)
# v14 reconciles automatically so here we force discrepancy
# with invoice_form.edit(0) as inv_form:
invoice_form.invoice_date = datetime.now()
with invoice_form.invoice_line_ids.edit(0) as line_form:
line_form.price_unit = 99
invoice = invoice_form.save()
invoice.action_post()
self.assertTrue(po.unreconciled)
po.action_reconcile()
po._compute_unreconciled()
self.assertFalse(po.unreconciled)
def test_05_button_done_reconcile(self):
"""Test auto reconcile when locking po."""
po = self.po_2.copy()
po.company_id.purchase_reconcile_account_id = self.writeoff_acc
po.button_confirm()
self._do_picking(po.picking_ids, fields.Datetime.now())
# Invoice created and validated:
# Odoo reconciles automatically so here we force discrepancy
po.action_create_invoice()
invoice_form = Form(
po.invoice_ids.filtered(lambda i: i.move_type == "in_invoice")[0]
)
invoice_form.invoice_date = datetime.now()
with invoice_form.invoice_line_ids.edit(0) as line_form:
line_form.price_unit = 99
invoice = invoice_form.save()
invoice.action_post()
self.assertTrue(po.unreconciled)
# check error if raised if not write-off account
with self.assertRaises(exceptions.ValidationError):
self.company.purchase_reconcile_account_id = False
po.button_done()
# restore the write off account
self.company.purchase_reconcile_account_id = self.writeoff_acc
po.button_done()
po._compute_unreconciled()
self.assertFalse(po.unreconciled)
def test_06_dropship_not_reconcile_sale_journal_items(self):
"""
Create a fake dropship and lock the PO before receiving the customer
invoice. The PO should not close the stock interim output account
"""
# to create the fake dropship we create a delivery and attach the
# journals to the purchase order craeted later
self.env["stock.quant"].create(
{
"product_id": self.product_to_reconcile.id,
"location_id": self.env.ref("stock.stock_location_stock").id,
"quantity": 1.0,
}
)
delivery = self._create_delivery(self.product_to_reconcile, 1)
self._do_picking(delivery, fields.Datetime.now())
# We create the PO now and receive it
po = self.po_2.copy()
po.button_confirm()
self._do_picking(po.picking_ids, fields.Datetime.now())
self.assertTrue(po.unreconciled)
# as long stock_dropshipping is not dependency, I force the PO to be in
# the journal items of the delivery
delivery_name = delivery.name
delivery_ji = self.env["account.move.line"].search(
[("move_id.ref", "=", delivery_name)]
)
delivery_ji.write(
{"purchase_line_id": po.order_line[0], "purchase_order_id": po.id}
)
# then I lock the po to force reconciliation
po.button_done()
po._compute_unreconciled()
self.assertFalse(po.unreconciled)
# the PO is reconciled and the stock interim deliverd account is not
# reconciled yet
for jii in delivery_ji:
self.assertFalse(jii.reconciled)
def test_07_multicompany(self):
"""
Force the company in the vendor bill to be wrong. The system will
write-off the journals for the shipment because those are the only ones
with the correct company
"""
po = self.po.copy()
po.button_confirm()
self._do_picking(po.picking_ids, fields.Datetime.now())
# Invoice created and validated:
f = Form(self.account_move_obj.with_context(default_move_type="in_invoice"))
f.partner_id = po.partner_id
f.invoice_date = fields.Date().today()
f.purchase_vendor_bill_id = self.env["purchase.bill.union"].browse(-po.id)
invoice = f.save()
chicago_journal = self.env["account.journal"].create(
{
"name": "chicago",
"code": "ref",
"type": "purchase",
"company_id": self.ref("stock.res_company_1"),
}
)
invoice.write(
{
"name": "/",
}
)
invoice.write(
{
"company_id": self.ref("stock.res_company_1"),
"journal_id": chicago_journal.id,
}
)
invoice.action_post()
self.assertEqual(po.state, "purchase")
# The bill is wrong so this is unreconciled
self.assertTrue(po.unreconciled)
po.button_done()
po._compute_unreconciled()
self.assertFalse(po.unreconciled)
# we check all the journals for the po have the same company
ji = self.env["account.move.line"].search(
[("purchase_order_id", "=", po.id), ("move_id", "!=", invoice.id)]
)
self.assertEqual(po.company_id, ji.mapped("company_id"))
def test_08_reconcile_by_product(self):
"""
Create a write-off by product
"""
po = self.po.copy()
po.write(
{
"order_line": [
(
0,
0,
{
"product_id": self.product_to_reconcile2.id,
"name": self.product_to_reconcile2.name,
"product_qty": 5.0,
"price_unit": 100.0,
"product_uom": self.product_to_reconcile.uom_id.id,
"date_planned": fields.Datetime.now(),
},
)
],
}
)
po.button_confirm()
self._do_picking(po.picking_ids, fields.Datetime.now())
# Invoice created and validated:
f = Form(self.account_move_obj.with_context(default_move_type="in_invoice"))
f.partner_id = po.partner_id
f.invoice_date = fields.Date().today()
f.purchase_vendor_bill_id = self.env["purchase.bill.union"].browse(-po.id)
invoice = f.save()
# force discrepancies
with f.invoice_line_ids.edit(0) as line_form:
line_form.price_unit = 99
with f.invoice_line_ids.edit(0) as line_form:
line_form.price_unit = 99
invoice = f.save()
invoice._post()
# The bill is different price so this is unreconciled
po._compute_unreconciled()
self.assertTrue(po.unreconciled)
po.button_done()
po._compute_unreconciled()
self.assertFalse(po.unreconciled)
# we check all the journals are balanced by product
ji_p1 = self.env["account.move.line"].search(
[
("purchase_order_id", "=", po.id),
("product_id", "=", self.product_to_reconcile.id),
("account_id", "=", self.account_grni.id),
]
)
ji_p2 = self.env["account.move.line"].search(
[
("purchase_order_id", "=", po.id),
("product_id", "=", self.product_to_reconcile2.id),
("account_id", "=", self.account_grni.id),
]
)
self.assertEqual(sum(ji_p1.mapped("balance")), 0.0)
self.assertEqual(sum(ji_p2.mapped("balance")), 0.0)

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="purchase_order_form" model="ir.ui.view">
<field name="name">purchase.order.form - purchase_unreconciled</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form" />
<field name="arch" type="xml">
<field name="invoice_status" position="after">
<field name="unreconciled" />
</field>
<div name="button_box" position="inside">
<button
type="object"
string="Unreconciled Journal Items"
name="action_view_unreconciled"
groups="account.group_account_manager"
class="oe_stat_button"
icon="fa-gears"
invisible="not unreconciled"
>
</button>
<button
type="object"
string="Reconcile"
name="action_reconcile"
groups="account.group_account_manager"
class="oe_stat_button"
icon="fa-link"
invisible="not unreconciled or state != 'done'"
>
</button>
</div>
</field>
</record>
<record id="purchase_order_view_search" model="ir.ui.view">
<field name="name">Custom Purchase Unreconciled Search</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_view_search" />
<field name="arch" type="xml">
<filter name="order_date" position="after">
<filter
name="unreconciled"
string="Unreconciled"
domain="[('unreconciled','=', True)]"
groups="account.group_account_manager"
help="Purchase orders with unreconciled journal items."
/>
</filter>
</field>
</record>
<record id="unreconciled_amount_purchase_order_view_tree" model="ir.ui.view">
<field name="name">unreconciled.amount.purchase.order.tree</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_view_tree" />
<field name="arch" type="xml">
<field name="invoice_status" position="after">
<field name="amount_unreconciled" sum="sum_ar" optional="hide" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.purchase.unreconciled</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//block[@id='bank_cash']/.." position="after">
<block title="Purchase Reconciling" id="purchase_reconcile">
<setting
id="purchase_lock_auto_reconcile"
help="Reconcile PO upon locking"
company_dependent="1"
>
<field name="purchase_lock_auto_reconcile" />
</setting>
<setting id="purchase_reconcile_account_id">
<field name="purchase_reconcile_account_id" />
</setting>
<setting id="purchase_reconcile_journal_id">
<field name="purchase_reconcile_journal_id" />
</setting>
<setting id="purchase_reconcile_tolerance">
<field name="purchase_reconcile_tolerance" />
</setting>
</block>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
from . import purchase_unreconciled_exceeded

View File

@@ -0,0 +1,36 @@
from odoo import _, fields, models
class PurchaseUnreconciledExceededWiz(models.TransientModel):
_name = "purchase.unreconciled.exceeded.wiz"
_description = "Purchase Unreconciled Exceeded Wizard"
purchase_id = fields.Many2one(
comodel_name="purchase.order", readonly=True, string="Order Number"
)
exception_msg = fields.Text(readonly=True)
origin_reference = fields.Reference(
lambda self: [
(m.model, m.name) for m in self.env["ir.model"].sudo().search([])
],
string="Object",
)
continue_method = fields.Char()
def action_show(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": _("Purchase unreconciled exceeded"),
"res_model": self._name,
"res_id": self.id,
"view_mode": "form",
"target": "new",
}
def button_continue(self):
self.ensure_one()
return getattr(
self.origin_reference.with_context(bypass_unreconciled=True),
self.continue_method,
)()

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="purchase_unreconciled_exceeded_wizard" model="ir.ui.view">
<field name="name">purchase unreconciled exceeded</field>
<field name="model">purchase.unreconciled.exceeded.wiz</field>
<field name="arch" type="xml">
<form string="Amount Unreconciled exceeded">
<p>The order has exceeded its amount unreconciled</p>
<field name="exception_msg" colspan="2" nolabel="1" />
<group>
<field name="purchase_id" />
</group>
<footer>
<button
string="Lock and Reconcile"
class="oe_highlight"
name="button_continue"
type="object"
groups="account.group_account_user"
/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>