Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[15.0][ADD] account_reconcile_sale_order #666

Open
wants to merge 1 commit into
base: 15.0
Choose a base branch
from
Open
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
119 changes: 119 additions & 0 deletions account_reconcile_sale_order/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
=====================
Reconcile sale orders
=====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:56395133d4d660c38418fe1fcfd4a84ba5403d7023e50c6531a16f25d947928a
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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--reconcile-lightgray.png?logo=github
:target: https://github.com/OCA/account-reconcile/tree/15.0/account_reconcile_sale_order
:alt: OCA/account-reconcile
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-reconcile-15-0/account-reconcile-15-0-account_reconcile_sale_order
: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-reconcile&target_branch=15.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allows a workflow where you don't invoice sale orders until
you've received a payment.

That's useful ie for webshops with non-instant payment like wire
transfer, where you might have a lot of customers not doing the payment
after all, which results in extra work for cancellation of the
invoices/orders involved.

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

Configuration
=============

To configure this module, you need to:

1. Go to Invoicing/Configuration/Reconciliation Models
2. Create a model of type *Rule to match sale orders*

Usage
=====

To use this module, you need to:

1. Have a payment on a bank statement matching the amount of an
*invoicable* sale order
2. Enter the reconciliation screen
3. Observe that the sale order is offered as reconciliation counterpart

Note the reconciliation only works if fully invoicing the sale order
yields an invoice over the order's total amount. Usually this means that
all products in the sale order must have invoicing policy *Ordered
quantities*.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-reconcile/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-reconcile/issues/new?body=module:%20account_reconcile_sale_order%0Aversion:%2015.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
-------

* Hunki Enterprises BV

Contributors
------------

- Holger Brunn <[email protected]>
(https://hunki-enterprises.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-hbrunn| image:: https://github.com/hbrunn.png?size=40px
:target: https://github.com/hbrunn
:alt: hbrunn

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-hbrunn|

This module is part of the `OCA/account-reconcile <https://github.com/OCA/account-reconcile/tree/15.0/account_reconcile_sale_order>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions account_reconcile_sale_order/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
29 changes: 29 additions & 0 deletions account_reconcile_sale_order/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2024 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)

{
"name": "Reconcile sale orders",
"summary": "Invoice and reconcile sale orders",
"version": "15.0.1.0.0",
"development_status": "Alpha",
"category": "Accounting",
"website": "https://github.com/OCA/account-reconcile",
"author": "Hunki Enterprises BV, Odoo Community Association (OCA)",
"maintainers": ["hbrunn"],
"license": "AGPL-3",
"depends": [
"sale",
"account_reconciliation_widget",
],
"data": [
"views/account_reconcile_model.xml",
],
"demo": [
"demo/account_reconcile_model.xml",
],
"assets": {
"web.assets_backend": [
"account_reconcile_sale_order/static/src/js/*.js",
],
},
}
6 changes: 6 additions & 0 deletions account_reconcile_sale_order/demo/account_reconcile_model.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<odoo>
<record id="reconcile_model_sale_order" model="account.reconcile.model">
<field name="name">Match sale orders</field>
<field name="rule_type">sale_order_matching</field>
</record>
</odoo>
3 changes: 3 additions & 0 deletions account_reconcile_sale_order/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import account_bank_statement_line
from . import account_reconciliation_widget
from . import account_reconcile_model
64 changes: 64 additions & 0 deletions account_reconcile_sale_order/models/account_bank_statement_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2024 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)

from odoo import models


class AccountBankStatementLine(models.Model):
_inherit = "account.bank.statement.line"

def process_reconciliation(
self, counterpart_aml_dicts=None, payment_aml_rec=None, new_aml_dicts=None
):
"""
Invoice selected sale orders and use resulting move lines
"""
new_aml_dicts2 = []
counterpart_aml_dicts = (counterpart_aml_dicts or [])[:]
for new_aml_dict in new_aml_dicts or []:
sale_order_id = new_aml_dict.get("sale_order_id")
if sale_order_id:
order = self.env["sale.order"].browse(sale_order_id)
self._process_reconciliation_sale_order_invoice(order)
counterpart_aml_dicts += (
self._process_reconciliation_sale_order_counterparts(order)
)
else:
new_aml_dicts2.append(new_aml_dict)

Check warning on line 27 in account_reconcile_sale_order/models/account_bank_statement_line.py

View check run for this annotation

Codecov / codecov/patch

account_reconcile_sale_order/models/account_bank_statement_line.py#L27

Added line #L27 was not covered by tests

return super().process_reconciliation(
counterpart_aml_dicts=counterpart_aml_dicts,
payment_aml_rec=payment_aml_rec,
new_aml_dicts=new_aml_dicts2,
)

def _process_reconciliation_sale_order_invoice(self, order):
"""
Invoice selected sale orders and post the invoices
"""
clean_context = {
key: value
for key, value in self.env.context.items()
if key != "force_price_include"
}
order = order.with_context(clean_context) # pylint: disable=context-overridden
if order.state in ("draft", "sent"):
order.action_confirm()
invoices = order._create_invoices()
invoices.action_post()

def _process_reconciliation_sale_order_counterparts(self, order):
"""
Return counterpart aml dicts for sale order
"""
return [
{
"name": line.name,
"move_line": line,
"debit": line.credit,
"credit": line.debit,
"analytic_tag_ids": [(6, 0, line.analytic_tag_ids.ids)],
}
for line in order.mapped("invoice_ids.line_ids")
if line.account_id.user_type_id.type == "receivable"
]
124 changes: 124 additions & 0 deletions account_reconcile_sale_order/models/account_reconcile_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright 2024 Hunki Enterprises BV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)


from odoo import fields, models


class AccountReconcileModel(models.AbstractModel):
_inherit = "account.reconcile.model"

rule_type = fields.Selection(
selection_add=[("sale_order_matching", "Rule to match sale orders")],
ondelete={"sale_order_matching": "cascade"},
)
sale_order_matching_token_match = fields.Boolean(
string="Match tokens",
help="When this is activated, the statement line's label is split into words "
"and if one of those words match a sale order, it is considered a match. So "
"if the statement line's label is 'hello world', sale orders with names 'hello', "
"'world', 'some name containing hello', 'some name containing world' will be "
"considered matches, in that order",
)
sale_order_matching_token_length = fields.Integer(
string="Minimum token length",
default=3,
help="Set the minimum word length to search for. If you set this to 4, and the "
"statement line's label is 'hello you', it will only search for 'hello', not "
"for 'you'",
)

def _get_candidates(self, st_lines_with_partner, excluded_ids):
if self.rule_type == "sale_order_matching":
return self._get_candidates_sale_order(st_lines_with_partner, excluded_ids)
else:
return super()._get_candidates(st_lines_with_partner, excluded_ids)

def _get_rule_result(
self, st_line, candidates, aml_ids_to_exclude, reconciled_amls_ids, partner_map
):
if self.rule_type == "sale_order_matching":
return self._get_rule_result_sale_order(
st_line,
candidates,
aml_ids_to_exclude,
reconciled_amls_ids,
partner_map,
)
else:
return super()._get_rule_result(
st_line,
candidates,
aml_ids_to_exclude,
reconciled_amls_ids,
partner_map,
)

def _get_candidates_sale_order(self, st_lines_with_partner, excluded_ids):
"""Return candidates for matching sale orders"""
return {
line.id: self._get_candidates_sale_order_best_match(
line, partner, excluded_ids
)
for line, partner in st_lines_with_partner
}

def _get_candidates_sale_order_best_match(
self, bank_statement_line, partner, excluded_ids
):
"""Return one sale order that is considered the best match for some line and partner"""

def domain(extra_domain):
widget = self.env["account.reconciliation.widget"]
return widget._get_sale_orders_for_bank_statement_line_domain(
bank_statement_line.id,
partner.id,
amount=bank_statement_line.amount,
excluded_ids=excluded_ids,
extra_domain=extra_domain,
)

def search(domain):
return self.env["sale.order"].search(domain, limit=1)

def first(field, operator, tokens):
return sum(
(search([(field, operator, token)]) for token in tokens),
self.env["sale.order"],
)[:1]

ref = bank_statement_line.payment_ref
tokens = list(
filter(
lambda x: len(x) >= self.sale_order_matching_token_length, ref.split()
)
)

return (
search(domain([("name", "=ilike", ref)]))
or search(domain([("partner_id", "=ilike", ref)]))
or (
(first("name", "=ilike", tokens) or first("name", "ilike", tokens))
if self.sale_order_matching_token_match
else self.env["sale.order"]
)
)

def _get_rule_result_sale_order(
self, st_line, candidates, aml_ids_to_exclude, reconciled_amls_ids, partner_map
):
return (
{
"model": self,
"status": "sale_order_matching",
"aml_ids": candidates,
"write_off_vals": [
self.env[
"account.reconciliation.widget"
]._reconciliation_proposition_from_sale_order(order)
for order in candidates
],
},
set(),
set(),
)
Loading
Loading