Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
112 changes: 112 additions & 0 deletions account_billing_portal/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
======================
Account Billing Portal
======================

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

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

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

This module adds a portal view for account billings. It allows users to
change the portal billing report template via a configurable setting. It
also adds the ability to send emails to partners directly from billing
records with the billing report attached.

.. 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 choose the billing portal template:

- Go to *Invoicing → Configuration → Settings*.
- Set a value in **Choose Billing Email Template**. If set, this
template will be used when sending billing emails to
customers/vendors. If left empty, the default template from the
*Account Billing Portal* module will be used instead.
- Set a value in **Choose Billing Portal Report**. If set, this report
will be used in the billing portal and as the PDF attachment in
billing emails. If left empty, the standard report from the *Account
Billing* module will be used instead.

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

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

* Quartile

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

- `Quartile <https://www.quartile.co>`__:

- Aung Ko Ko Lin

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-yostashiro| image:: https://github.com/yostashiro.png?size=40px
:target: https://github.com/yostashiro
:alt: yostashiro
.. |maintainer-aungkokolin1997| image:: https://github.com/aungkokolin1997.png?size=40px
:target: https://github.com/aungkokolin1997
:alt: aungkokolin1997

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

|maintainer-yostashiro| |maintainer-aungkokolin1997|

This module is part of the `OCA/account-invoicing <https://github.com/OCA/account-invoicing/tree/18.0/account_billing_portal>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions account_billing_portal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
22 changes: 22 additions & 0 deletions account_billing_portal/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2025 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Account Billing Portal",
"version": "18.0.1.0.0",
"author": "Quartile, Odoo Community Association (OCA)",
"category": "Accounting",
"website": "https://github.com/OCA/account-invoicing",
"license": "AGPL-3",
"depends": ["account_billing"],
"data": [
"security/account_billing_portal_security.xml",
"security/ir.model.access.csv",
"data/mail_template_data.xml",
"views/account_billing_portal_templates.xml",
"views/account_billing_views.xml",
"views/res_config_settings_views.xml",
],
"maintainers": ["yostashiro", "aungkokolin1997"],
"development_status": "Alpha",
"installable": True,
}
1 change: 1 addition & 0 deletions account_billing_portal/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import portal
158 changes: 158 additions & 0 deletions account_billing_portal/controllers/portal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Copyright 2025 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from collections import OrderedDict

from odoo import _, http
from odoo.exceptions import AccessError, MissingError
from odoo.http import request

from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.portal.controllers.portal import pager as portal_pager


class CustomerPortalBilling(CustomerPortal):
def _show_report(self, model, report_type, report_ref, download=False):
if model._name != "account.billing":
return super()._show_report(model, report_type, report_ref, download)
billing_report = request.env.user.company_id.billing_portal_report
if billing_report:
external_id = billing_report.get_external_id()
report_ref = external_id.get(billing_report.id)
return super()._show_report(model, report_type, report_ref, download)

def _get_billing_domain(self, bill_type=None):
domain = [("state", "=", "billed")]
if bill_type:
domain.append(("bill_type", "=", bill_type))
return domain

def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
Billing = request.env["account.billing"]
if "customer_bill_count" in counters:
values["customer_bill_count"] = (
Billing.search_count(self._get_billing_domain("out_invoice"))
if Billing.has_access("read")
else 0
)
if "vendor_bill_count" in counters:
values["vendor_bill_count"] = (
Billing.search_count(self._get_billing_domain("in_invoice"))
if Billing.has_access("read")
else 0
)
return values

def _get_billing_searchbar_sortings(self):
return {
"date": {"label": _("Newest"), "order": "create_date desc, id desc"},
"billing_date": {"label": _("Billing Date"), "order": "date desc, id desc"},
"name": {"label": _("Name"), "order": "name asc, id asc"},
}

def _render_billing_portal(
self,
page,
sortby,
filterby,
searchbar_filters,
default_filter,
):
values = self._prepare_portal_layout_values()
Billing = request.env["account.billing"]
domain = self._get_billing_domain()
searchbar_sortings = self._get_billing_searchbar_sortings()
if not sortby:
sortby = "date"
order = searchbar_sortings[sortby]["order"]
if searchbar_filters:
if not filterby or filterby not in searchbar_filters:
filterby = default_filter
domain += searchbar_filters[filterby]["domain"]
count = Billing.search_count(domain)
pager = portal_pager(
url="/my/billings",
url_args={"sortby": sortby, "filterby": filterby},
total=count,
page=page,
step=self._items_per_page,
)
billings = Billing.search(
domain, order=order, limit=self._items_per_page, offset=pager["offset"]
)
request.session["my_billing_history"] = billings.ids[:100]
values.update(
{
"billings": billings,
"page_name": "billing",
"pager": pager,
"searchbar_sortings": searchbar_sortings,
"sortby": sortby,
"searchbar_filters": OrderedDict(sorted(searchbar_filters.items())),
"filterby": filterby,
"default_url": "/my/billings",
}
)
return request.render("account_billing_portal.portal_my_billings", values)

@http.route(
["/my/billings", "/my/billings/page/<int:page>"],
type="http",
auth="user",
website=True,
)
def portal_my_billings(self, page=1, sortby=None, filterby=None, **kw):
return self._render_billing_portal(
page,
sortby,
filterby,
{
"all": {
"label": _("All"),
"domain": [("state", "=", "billed")],
},
"out_invoice": {
"label": _("Customer Bills"),
"domain": [("bill_type", "=", "out_invoice")],
},
"in_invoice": {
"label": _("Vendor Bills"),
"domain": [("bill_type", "=", "in_invoice")],
},
},
"all",
)

def _billing_get_page_view_values(self, billing, access_token, **kwargs):
values = {
"billing": billing,
"page_name": "billing",
"report_type": "html",
}
return self._get_page_view_values(
billing, access_token, values, "my_billing_history", False, **kwargs
)

@http.route(
["/my/billings/<int:billing_id>"], type="http", auth="public", website=True
)
def portal_my_billing(
self, billing_id, access_token=None, report_type=None, download=False, **kw
):
try:
billing_sudo = self._document_check_access(
"account.billing", billing_id, access_token
)
except (AccessError, MissingError):
return request.redirect("/my")
if report_type in ("html", "pdf", "text"):
pdf_report_name = "account_billing.report_account_billing"
return self._show_report(
model=billing_sudo,
report_type=report_type,
report_ref=pdf_report_name,
download=download,
)
values = self._billing_get_page_view_values(billing_sudo, access_token, **kw)
return request.render("account_billing_portal.portal_my_billing", values)
37 changes: 37 additions & 0 deletions account_billing_portal/data/mail_template_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="email_template_billing" model="mail.template">
<field name="name">Billing</field>
<field name="model_id" ref="account_billing.model_account_billing" />
<field
name="subject"
>{{ object.company_id.name }} Billing (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="description">Sent billing to customer/vendor</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-out="object.partner_id.name or ''">Brandon Freeman</t>
<t t-if="object.partner_id.parent_id">
(<t t-out="object.partner_id.parent_id.name or ''">Azure Interior</t>)
</t>
<br /><br />
Please find attached the billing from <t
t-out="object.company_id.name or ''"
>YourCompany</t>.
<br /><br />
If you have any questions, please do not hesitate to contact us.
<br /><br />
Best regards,
<t t-if="not is_html_empty(env.user.signature)">
<br />
<br />
<t t-out="env.user.signature or ''" />
</t>
</p>
</div>
</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True" />
</record>
</odoo>
Loading