Browse Source

[IMP] partner_statement: black, isort

pull/654/head
mreficent 4 years ago
parent
commit
320791be21
  1. 38
      partner_statement/__manifest__.py
  2. 2
      partner_statement/readme/DESCRIPTION.rst
  3. 95
      partner_statement/report/activity_statement.py
  4. 55
      partner_statement/report/outstanding_statement.py
  5. 300
      partner_statement/report/report_statement_common.py
  6. 9
      partner_statement/security/statement_security.xml
  7. 6
      partner_statement/static/src/scss/layout_statement.scss
  8. 96
      partner_statement/tests/test_activity_statement.py
  9. 78
      partner_statement/tests/test_outstanding_statement.py
  10. 31
      partner_statement/tests/test_res_config_settings.py
  11. 228
      partner_statement/views/activity_statement.xml
  12. 43
      partner_statement/views/aging_buckets.xml
  13. 16
      partner_statement/views/assets.xml
  14. 221
      partner_statement/views/outstanding_statement.xml
  15. 64
      partner_statement/views/res_config_settings.xml
  16. 21
      partner_statement/wizard/activity_statement_wizard.py
  17. 11
      partner_statement/wizard/outstanding_statement_wizard.py
  18. 32
      partner_statement/wizard/res_config_settings.py
  19. 54
      partner_statement/wizard/statement_common.py
  20. 104
      partner_statement/wizard/statement_wizard.xml

38
partner_statement/__manifest__.py

@ -3,25 +3,23 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
'name': 'Partner Statement',
'version': '12.0.1.0.5',
'category': 'Accounting & Finance',
'summary': 'OCA Financial Reports',
'author': "Eficent, Odoo Community Association (OCA)",
'website': 'https://github.com/OCA/account-financial-reporting',
'license': 'AGPL-3',
'depends': [
'account',
"name": "Partner Statement",
"version": "12.0.1.0.5",
"category": "Accounting & Finance",
"summary": "OCA Financial Reports",
"author": "Eficent, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-financial-reporting",
"license": "AGPL-3",
"depends": ["account"],
"data": [
"security/statement_security.xml",
"views/activity_statement.xml",
"views/outstanding_statement.xml",
"views/assets.xml",
"views/aging_buckets.xml",
"views/res_config_settings.xml",
"wizard/statement_wizard.xml",
],
'data': [
'security/statement_security.xml',
'views/activity_statement.xml',
'views/outstanding_statement.xml',
'views/assets.xml',
'views/aging_buckets.xml',
'views/res_config_settings.xml',
'wizard/statement_wizard.xml',
],
'installable': True,
'application': False,
"installable": True,
"application": False,
}

2
partner_statement/readme/DESCRIPTION.rst

@ -1,4 +1,4 @@
This module extends the functionality of Invoicing to support the printing of customer and vendor statements.
This module extends the functionality of Invoicing to support the printing of customer and vendor statements.
There are two types of statements, Activity and Outstanding. Aging details can be shown in the reports, expressed in aging buckets,
so the customer or vendor can review how much is open, due or overdue.

95
partner_statement/report/activity_statement.py

@ -2,18 +2,21 @@
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models
from collections import defaultdict
from odoo import api, models
class ActivityStatement(models.AbstractModel):
"""Model of Activity Statement"""
_inherit = 'statement.common'
_name = 'report.partner_statement.activity_statement'
_inherit = "statement.common"
_name = "report.partner_statement.activity_statement"
def _initial_balance_sql_q1(self, partners, date_start, account_type):
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT l.partner_id, l.currency_id, l.company_id,
CASE WHEN l.currency_id is not null AND l.amount_currency > 0.0
THEN sum(l.amount_currency)
@ -31,34 +34,50 @@ class ActivityStatement(models.AbstractModel):
AND m.state IN ('posted')
GROUP BY l.partner_id, l.currency_id, l.amount_currency,
l.company_id
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _initial_balance_sql_q2(self, company_id):
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT Q1.partner_id, debit-credit AS balance,
COALESCE(Q1.currency_id, c.currency_id) AS currency_id
FROM Q1
JOIN res_company c ON (c.id = Q1.company_id)
WHERE c.id = %(company_id)s
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _get_account_initial_balance(self, company_id, partner_ids,
date_start, account_type):
def _get_account_initial_balance(
self, company_id, partner_ids, date_start, account_type
):
balance_start = defaultdict(list)
partners = tuple(partner_ids)
# pylint: disable=E8103
self.env.cr.execute("""WITH Q1 AS (%s), Q2 AS (%s)
self.env.cr.execute(
"""WITH Q1 AS (%s), Q2 AS (%s)
SELECT partner_id, currency_id, balance
FROM Q2""" % (self._initial_balance_sql_q1(partners, date_start,
account_type),
self._initial_balance_sql_q2(company_id)))
FROM Q2"""
% (
self._initial_balance_sql_q1(partners, date_start, account_type),
self._initial_balance_sql_q2(company_id),
)
)
for row in self.env.cr.dictfetchall():
balance_start[row.pop('partner_id')].append(row)
balance_start[row.pop("partner_id")].append(row)
return balance_start
def _display_lines_sql_q1(self, partners, date_start, date_end,
account_type):
return str(self._cr.mogrify("""
def _display_lines_sql_q1(self, partners, date_start, date_end, account_type):
return str(
self._cr.mogrify(
"""
SELECT m.name AS move_id, l.partner_id, l.date,
CASE WHEN (aj.type IN ('sale', 'purchase'))
THEN l.name
@ -104,10 +123,16 @@ class ActivityStatement(models.AbstractModel):
ELSE ''
END,
l.blocked, l.currency_id, l.amount_currency, l.company_id
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _display_lines_sql_q2(self, company_id):
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT Q1.partner_id, Q1.move_id, Q1.date, Q1.date_maturity,
Q1.name, Q1.ref, Q1.debit, Q1.credit,
Q1.debit-Q1.credit as amount, Q1.blocked,
@ -115,36 +140,46 @@ class ActivityStatement(models.AbstractModel):
FROM Q1
JOIN res_company c ON (c.id = Q1.company_id)
WHERE c.id = %(company_id)s
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _get_account_display_lines(self, company_id, partner_ids, date_start,
date_end, account_type):
def _get_account_display_lines(
self, company_id, partner_ids, date_start, date_end, account_type
):
res = dict(map(lambda x: (x, []), partner_ids))
partners = tuple(partner_ids)
# pylint: disable=E8103
self.env.cr.execute("""
self.env.cr.execute(
"""
WITH Q1 AS (%s),
Q2 AS (%s)
SELECT partner_id, move_id, date, date_maturity, name, ref, debit,
credit, amount, blocked, currency_id
FROM Q2
ORDER BY date, date_maturity, move_id""" % (
self._display_lines_sql_q1(partners, date_start, date_end,
account_type),
self._display_lines_sql_q2(company_id)))
ORDER BY date, date_maturity, move_id"""
% (
self._display_lines_sql_q1(
partners, date_start, date_end, account_type
),
self._display_lines_sql_q2(company_id),
)
)
for row in self.env.cr.dictfetchall():
res[row.pop('partner_id')].append(row)
res[row.pop("partner_id")].append(row)
return res
@api.multi
def _get_report_values(self, docids, data):
if not data:
data = {}
if 'company_id' not in data:
if "company_id" not in data:
wiz = self.env["activity.statement.wizard"].with_context(
active_ids=docids, model="res.partner"
)
data.update(wiz.create({})._prepare_statement())
data['amount_field'] = 'amount'
data["amount_field"] = "amount"
return super()._get_report_values(docids, data)

55
partner_statement/report/outstanding_statement.py

@ -8,12 +8,14 @@ from odoo import api, models
class OutstandingStatement(models.AbstractModel):
"""Model of Outstanding Statement"""
_inherit = 'statement.common'
_name = 'report.partner_statement.outstanding_statement'
_inherit = "statement.common"
_name = "report.partner_statement.outstanding_statement"
def _display_lines_sql_q1(self, partners, date_end, account_type):
partners = tuple(partners)
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT m.name AS move_id, l.partner_id, l.date, l.name,
l.ref, l.blocked, l.currency_id, l.company_id,
CASE WHEN (l.currency_id is not null AND l.amount_currency > 0.0)
@ -62,12 +64,16 @@ class OutstandingStatement(models.AbstractModel):
GROUP BY l.partner_id, m.name, l.date, l.date_maturity, l.name,
l.ref, l.blocked, l.currency_id,
l.balance, l.amount_currency, l.company_id
""", locals()), "utf-8"
""",
locals(),
),
"utf-8",
)
def _display_lines_sql_q2(self):
return str(
self._cr.mogrify("""
self._cr.mogrify(
"""
SELECT Q1.partner_id, Q1.currency_id, Q1.move_id,
Q1.date, Q1.date_maturity, Q1.debit, Q1.credit,
Q1.name, Q1.ref, Q1.blocked, Q1.company_id,
@ -76,13 +82,16 @@ class OutstandingStatement(models.AbstractModel):
ELSE Q1.open_amount
END as open_amount
FROM Q1
""", locals()
""",
locals(),
),
"utf-8"
"utf-8",
)
def _display_lines_sql_q3(self, company_id):
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT Q2.partner_id, Q2.move_id, Q2.date, Q2.date_maturity,
Q2.name, Q2.ref, Q2.debit, Q2.credit,
Q2.debit-Q2.credit AS amount, blocked,
@ -91,37 +100,45 @@ class OutstandingStatement(models.AbstractModel):
FROM Q2
JOIN res_company c ON (c.id = Q2.company_id)
WHERE c.id = %(company_id)s AND Q2.open_amount != 0.0
""", locals()), "utf-8"
""",
locals(),
),
"utf-8",
)
def _get_account_display_lines(self, company_id, partner_ids, date_start,
date_end, account_type):
def _get_account_display_lines(
self, company_id, partner_ids, date_start, date_end, account_type
):
res = dict(map(lambda x: (x, []), partner_ids))
partners = tuple(partner_ids)
# pylint: disable=E8103
self.env.cr.execute("""
self.env.cr.execute(
"""
WITH Q1 as (%s),
Q2 AS (%s),
Q3 AS (%s)
SELECT partner_id, currency_id, move_id, date, date_maturity, debit,
credit, amount, open_amount, name, ref, blocked
FROM Q3
ORDER BY date, date_maturity, move_id""" % (
self._display_lines_sql_q1(partners, date_end, account_type),
self._display_lines_sql_q2(),
self._display_lines_sql_q3(company_id)))
ORDER BY date, date_maturity, move_id"""
% (
self._display_lines_sql_q1(partners, date_end, account_type),
self._display_lines_sql_q2(),
self._display_lines_sql_q3(company_id),
)
)
for row in self.env.cr.dictfetchall():
res[row.pop('partner_id')].append(row)
res[row.pop("partner_id")].append(row)
return res
@api.multi
def _get_report_values(self, docids, data):
if not data:
data = {}
if 'company_id' not in data:
if "company_id" not in data:
wiz = self.env["outstanding.statement.wizard"].with_context(
active_ids=docids, model="res.partner"
)
data.update(wiz.create({})._prepare_statement())
data['amount_field'] = 'open_amount'
data["amount_field"] = "open_amount"
return super()._get_report_values(docids, data)

300
partner_statement/report/report_statement_common.py

@ -3,39 +3,42 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import datetime, timedelta
from odoo import _, api, fields, models
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
from odoo import api, fields, models, _
class ReportStatementCommon(models.AbstractModel):
"""Abstract Report Statement for use in other models"""
_name = 'statement.common'
_description = 'Statement Reports Common'
_name = "statement.common"
_description = "Statement Reports Common"
def _get_invoice_address(self, part):
inv_addr_id = part.address_get(['invoice']).get('invoice', part.id)
inv_addr_id = part.address_get(["invoice"]).get("invoice", part.id)
return self.env["res.partner"].browse(inv_addr_id)
def _format_date_to_partner_lang(
self,
date,
date_format=DEFAULT_SERVER_DATE_FORMAT
self, date, date_format=DEFAULT_SERVER_DATE_FORMAT
):
if isinstance(date, str):
date = datetime.strptime(date, DEFAULT_SERVER_DATE_FORMAT)
return date.strftime(date_format) if date else ''
return date.strftime(date_format) if date else ""
def _get_account_display_lines(self, company_id, partner_ids, date_start,
date_end, account_type):
def _get_account_display_lines(
self, company_id, partner_ids, date_start, date_end, account_type
):
raise NotImplementedError
def _get_account_initial_balance(self, company_id, partner_ids,
date_start, account_type):
def _get_account_initial_balance(
self, company_id, partner_ids, date_start, account_type
):
return {}
def _show_buckets_sql_q1(self, partners, date_end, account_type):
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT l.partner_id, l.currency_id, l.company_id, l.move_id,
CASE WHEN l.balance > 0.0
THEN l.balance - sum(coalesce(pd.amount, 0.0))
@ -76,11 +79,16 @@ class ReportStatementCommon(models.AbstractModel):
GROUP BY l.partner_id, l.currency_id, l.date, l.date_maturity,
l.amount_currency, l.balance, l.move_id,
l.company_id, l.id
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _show_buckets_sql_q2(self, date_end, minus_30, minus_60, minus_90,
minus_120):
return str(self._cr.mogrify("""
def _show_buckets_sql_q2(self, date_end, minus_30, minus_60, minus_90, minus_120):
return str(
self._cr.mogrify(
"""
SELECT partner_id, currency_id, date_maturity, open_due,
open_due_currency, move_id, company_id,
CASE
@ -146,17 +154,27 @@ class ReportStatementCommon(models.AbstractModel):
FROM Q1
GROUP BY partner_id, currency_id, date_maturity, open_due,
open_due_currency, move_id, company_id
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _show_buckets_sql_q3(self, company_id):
return str(self._cr.mogrify("""
return str(
self._cr.mogrify(
"""
SELECT Q2.partner_id, current, b_1_30, b_30_60, b_60_90, b_90_120,
b_over_120,
COALESCE(Q2.currency_id, c.currency_id) AS currency_id
FROM Q2
JOIN res_company c ON (c.id = Q2.company_id)
WHERE c.id = %(company_id)s
""", locals()), "utf-8")
""",
locals(),
),
"utf-8",
)
def _show_buckets_sql_q4(self):
return """
@ -172,41 +190,36 @@ class ReportStatementCommon(models.AbstractModel):
def _get_bucket_dates(self, date_end, aging_type):
return getattr(
self, '_get_bucket_dates_%s' % aging_type,
self._get_bucket_dates_days
self, "_get_bucket_dates_%s" % aging_type, self._get_bucket_dates_days
)(date_end)
def _get_bucket_dates_days(self, date_end):
return {
'date_end': date_end,
'minus_30': date_end - timedelta(days=30),
'minus_60': date_end - timedelta(days=60),
'minus_90': date_end - timedelta(days=90),
'minus_120': date_end - timedelta(days=120),
"date_end": date_end,
"minus_30": date_end - timedelta(days=30),
"minus_60": date_end - timedelta(days=60),
"minus_90": date_end - timedelta(days=90),
"minus_120": date_end - timedelta(days=120),
}
def _get_bucket_dates_months(self, date_end):
res = {}
d = date_end
for k in (
"date_end",
"minus_30",
"minus_60",
"minus_90",
"minus_120",
):
for k in ("date_end", "minus_30", "minus_60", "minus_90", "minus_120"):
res[k] = d
d = d.replace(day=1) - timedelta(days=1)
return res
def _get_account_show_buckets(self, company_id, partner_ids, date_end,
account_type, aging_type):
def _get_account_show_buckets(
self, company_id, partner_ids, date_end, account_type, aging_type
):
buckets = dict(map(lambda x: (x, []), partner_ids))
partners = tuple(partner_ids)
full_dates = self._get_bucket_dates(date_end, aging_type)
# pylint: disable=E8103
# All input queries are properly escaped - false positive
self.env.cr.execute("""
self.env.cr.execute(
"""
WITH Q1 AS (%s),
Q2 AS (%s),
Q3 AS (%s),
@ -217,63 +230,63 @@ class ReportStatementCommon(models.AbstractModel):
AS balance
FROM Q4
GROUP BY partner_id, currency_id, current, b_1_30, b_30_60,
b_60_90, b_90_120, b_over_120""" % (
self._show_buckets_sql_q1(partners, date_end, account_type),
self._show_buckets_sql_q2(
full_dates['date_end'],
full_dates['minus_30'],
full_dates['minus_60'],
full_dates['minus_90'],
full_dates['minus_120']),
self._show_buckets_sql_q3(company_id),
self._show_buckets_sql_q4()))
b_60_90, b_90_120, b_over_120"""
% (
self._show_buckets_sql_q1(partners, date_end, account_type),
self._show_buckets_sql_q2(
full_dates["date_end"],
full_dates["minus_30"],
full_dates["minus_60"],
full_dates["minus_90"],
full_dates["minus_120"],
),
self._show_buckets_sql_q3(company_id),
self._show_buckets_sql_q4(),
)
)
for row in self.env.cr.dictfetchall():
buckets[row.pop('partner_id')].append(row)
buckets[row.pop("partner_id")].append(row)
return buckets
def _get_bucket_labels(self, date_end, aging_type):
return getattr(
self, '_get_bucket_labels_%s' % aging_type,
self._get_bucket_dates_days
self, "_get_bucket_labels_%s" % aging_type, self._get_bucket_dates_days
)(date_end)
def _get_bucket_labels_days(self, date_end):
return [
_('Current'),
_('1 - 30 Days'),
_('31 - 60 Days'),
_('61 - 90 Days'),
_('91 - 120 Days'),
_('121 Days +'),
_('Total'),
_("Current"),
_("1 - 30 Days"),
_("31 - 60 Days"),
_("61 - 90 Days"),
_("91 - 120 Days"),
_("121 Days +"),
_("Total"),
]
def _get_bucket_labels_months(self, date_end):
return [
_('Current'),
_('1 Month'),
_('2 Months'),
_('3 Months'),
_('4 Months'),
_('Older'),
_('Total'),
_("Current"),
_("1 Month"),
_("2 Months"),
_("3 Months"),
_("4 Months"),
_("Older"),
_("Total"),
]
def _get_line_currency_defaults(self, currency_id, currencies,
balance_forward):
def _get_line_currency_defaults(self, currency_id, currencies, balance_forward):
if currency_id not in currencies:
# This will only happen if currency is inactive
currencies[currency_id] = (
self.env['res.currency'].browse(currency_id)
)
currencies[currency_id] = self.env["res.currency"].browse(currency_id)
return (
{
'lines': [],
'buckets': [],
'balance_forward': balance_forward,
'amount_due': balance_forward,
"lines": [],
"buckets": [],
"balance_forward": balance_forward,
"amount_due": balance_forward,
},
currencies
currencies,
)
@api.multi
@ -298,45 +311,48 @@ class ReportStatementCommon(models.AbstractModel):
}
}
"""
company_id = data['company_id']
partner_ids = data['partner_ids']
date_start = data.get('date_start')
company_id = data["company_id"]
partner_ids = data["partner_ids"]
date_start = data.get("date_start")
if date_start and isinstance(date_start, str):
date_start = datetime.strptime(
date_start, DEFAULT_SERVER_DATE_FORMAT
).date()
date_end = data['date_end']
date_end = data["date_end"]
if isinstance(date_end, str):
date_end = datetime.strptime(
date_end, DEFAULT_SERVER_DATE_FORMAT
).date()
account_type = data['account_type']
aging_type = data['aging_type']
date_end = datetime.strptime(date_end, DEFAULT_SERVER_DATE_FORMAT).date()
account_type = data["account_type"]
aging_type = data["aging_type"]
today = fields.Date.today()
amount_field = data.get('amount_field', 'amount')
amount_field = data.get("amount_field", "amount")
# There should be relatively few of these, so to speed performance
# we cache them - default needed if partner lang not set
self._cr.execute("""
self._cr.execute(
"""
SELECT p.id, l.date_format
FROM res_partner p LEFT JOIN res_lang l ON p.lang=l.code
WHERE p.id IN %(partner_ids)s
""", {"partner_ids": tuple(partner_ids)})
""",
{"partner_ids": tuple(partner_ids)},
)
date_formats = {r[0]: r[1] for r in self._cr.fetchall()}
default_fmt = self.env["res.lang"]._lang_get(
self.env.user.lang).date_format
currencies = {x.id: x for x in self.env['res.currency'].search([])}
default_fmt = self.env["res.lang"]._lang_get(self.env.user.lang).date_format
currencies = {x.id: x for x in self.env["res.currency"].search([])}
res = {}
# get base data
lines = self._get_account_display_lines(
company_id, partner_ids, date_start, date_end, account_type)
company_id, partner_ids, date_start, date_end, account_type
)
balances_forward = self._get_account_initial_balance(
company_id, partner_ids, date_start, account_type)
company_id, partner_ids, date_start, account_type
)
if data['show_aging_buckets']:
if data["show_aging_buckets"]:
buckets = self._get_account_show_buckets(
company_id, partner_ids, date_end, account_type, aging_type)
company_id, partner_ids, date_end, account_type, aging_type
)
bucket_labels = self._get_bucket_labels(date_end, aging_type)
else:
bucket_labels = {}
@ -346,69 +362,65 @@ class ReportStatementCommon(models.AbstractModel):
partners_to_remove = set()
for partner_id in partner_ids:
res[partner_id] = {
'today': format_date(
today,
date_formats.get(partner_id, default_fmt)
),
'start': format_date(
date_start,
date_formats.get(partner_id, default_fmt)
),
'end': format_date(
date_end,
date_formats.get(partner_id, default_fmt)
"today": format_date(today, date_formats.get(partner_id, default_fmt)),
"start": format_date(
date_start, date_formats.get(partner_id, default_fmt)
),
'currencies': {},
"end": format_date(date_end, date_formats.get(partner_id, default_fmt)),
"currencies": {},
}
currency_dict = res[partner_id]['currencies']
currency_dict = res[partner_id]["currencies"]
for line in balances_forward.get(partner_id, []):
currency_dict[line['currency_id']], currencies = (
self._get_line_currency_defaults(
line['currency_id'], currencies, line['balance'])
(
currency_dict[line["currency_id"]],
currencies,
) = self._get_line_currency_defaults(
line["currency_id"], currencies, line["balance"]
)
for line in lines[partner_id]:
if line['currency_id'] not in currency_dict:
currency_dict[line['currency_id']], currencies = (
self._get_line_currency_defaults(
line['currency_id'], currencies, 0.0)
if line["currency_id"] not in currency_dict:
(
currency_dict[line["currency_id"]],
currencies,
) = self._get_line_currency_defaults(
line["currency_id"], currencies, 0.0
)
line_currency = currency_dict[line['currency_id']]
if not line['blocked']:
line_currency['amount_due'] += line[amount_field]
line['balance'] = line_currency['amount_due']
line['date'] = format_date(
line['date'], date_formats.get(partner_id, default_fmt)
line_currency = currency_dict[line["currency_id"]]
if not line["blocked"]:
line_currency["amount_due"] += line[amount_field]
line["balance"] = line_currency["amount_due"]
line["date"] = format_date(
line["date"], date_formats.get(partner_id, default_fmt)
)
line['date_maturity'] = format_date(
line['date_maturity'],
date_formats.get(partner_id, default_fmt)
line["date_maturity"] = format_date(
line["date_maturity"], date_formats.get(partner_id, default_fmt)
)
line_currency['lines'].append(line)
line_currency["lines"].append(line)
if data['show_aging_buckets']:
if data["show_aging_buckets"]:
for line in buckets[partner_id]:
if line['currency_id'] not in currency_dict:
currency_dict[line['currency_id']], currencies = (
self._get_line_currency_defaults(
line['currency_id'], currencies, 0.0)
if line["currency_id"] not in currency_dict:
(
currency_dict[line["currency_id"]],
currencies,
) = self._get_line_currency_defaults(
line["currency_id"], currencies, 0.0
)
line_currency = currency_dict[line['currency_id']]
line_currency['buckets'] = line
line_currency = currency_dict[line["currency_id"]]
line_currency["buckets"] = line
if len(partner_ids) > 1:
values = currency_dict.values()
if not any(
[v['lines'] or v['balance_forward'] for v in values]
):
if not any([v["lines"] or v["balance_forward"] for v in values]):
if data["filter_non_due_partners"]:
partners_to_remove.add(partner_id)
continue
else:
res[partner_id]['no_entries'] = True
res[partner_id]["no_entries"] = True
if data["filter_negative_balances"]:
if not all([v['amount_due'] >= 0.0 for v in values]):
if not all([v["amount_due"] >= 0.0 for v in values]):
partners_to_remove.add(partner_id)
for partner in partners_to_remove:
@ -416,13 +428,13 @@ class ReportStatementCommon(models.AbstractModel):
partner_ids.remove(partner)
return {
'doc_ids': partner_ids,
'doc_model': 'res.partner',
'docs': self.env['res.partner'].browse(partner_ids),
'data': res,
'company': self.env['res.company'].browse(company_id),
'Currencies': currencies,
'account_type': account_type,
'bucket_labels': bucket_labels,
'get_inv_addr': self._get_invoice_address,
"doc_ids": partner_ids,
"doc_model": "res.partner",
"docs": self.env["res.partner"].browse(partner_ids),
"data": res,
"company": self.env["res.company"].browse(company_id),
"Currencies": currencies,
"account_type": account_type,
"bucket_labels": bucket_labels,
"get_inv_addr": self._get_invoice_address,
}

9
partner_statement/security/statement_security.xml

@ -1,14 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="group_activity_statement" model="res.groups">
<field name="name">Use activity statements</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="category_id" ref="base.module_category_hidden" />
</record>
<record id="group_outstanding_statement" model="res.groups">
<field name="name">Use outstanding statements</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="category_id" ref="base.module_category_hidden" />
</record>
</odoo>

6
partner_statement/static/src/scss/layout_statement.scss

@ -1,16 +1,16 @@
.table-statement {
.amount {
text-align: right !important;
width: 14% //spread 7 columns evenly
width: 14%; //spread 7 columns evenly
}
thead {
border-bottom: solid; // required for clean layout
tr th:first-child {
width: auto !important; // required for clean layout
}
}
tr th:last-child {
width: 16% !important; // required for boxed layout
}
}
}
}

96
partner_statement/tests/test_activity_statement.py

@ -3,108 +3,112 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import date
from odoo.tests.common import TransactionCase
from odoo import fields
from odoo.tests.common import TransactionCase
class TestActivityStatement(TransactionCase):
"""
Tests for Activity Statement.
"""
def setUp(self):
super().setUp()
self.res_users_model = self.env['res.users']
self.company = self.env.ref('base.main_company')
self.partner1 = self.env.ref('base.res_partner_1')
self.partner2 = self.env.ref('base.res_partner_2')
self.g_account_user = self.env.ref('account.group_account_user')
self.res_users_model = self.env["res.users"]
self.company = self.env.ref("base.main_company")
self.partner1 = self.env.ref("base.res_partner_1")
self.partner2 = self.env.ref("base.res_partner_2")
self.g_account_user = self.env.ref("account.group_account_user")
self.user = self._create_user('user_1', [self.g_account_user],
self.company).id
self.user = self._create_user("user_1", [self.g_account_user], self.company).id
self.statement_model = \
self.env['report.partner_statement.activity_statement']
self.wiz = self.env['activity.statement.wizard']
self.report_name = 'partner_statement.activity_statement'
self.report_title = 'Activity Statement'
self.statement_model = self.env["report.partner_statement.activity_statement"]
self.wiz = self.env["activity.statement.wizard"]
self.report_name = "partner_statement.activity_statement"
self.report_title = "Activity Statement"
self.today = fields.Date.context_today(self.wiz)
def _create_user(self, login, groups, company):
group_ids = [group.id for group in groups]
user = self.res_users_model.create({
'name': login,
'login': login,
'email': 'example@yourcompany.com',
'company_id': company.id,
'company_ids': [(4, company.id)],
'groups_id': [(6, 0, group_ids)]
})
user = self.res_users_model.create(
{
"name": login,
"login": login,
"email": "example@yourcompany.com",
"company_id": company.id,
"company_ids": [(4, company.id)],
"groups_id": [(6, 0, group_ids)],
}
)
return user
def test_customer_activity_statement(self):
wiz_id = self.wiz.with_context(
active_ids=[self.partner1.id, self.partner2.id],
active_ids=[self.partner1.id, self.partner2.id]
).create({})
wiz_id.aging_type = 'months'
wiz_id.aging_type = "months"
wiz_id.show_aging_buckets = False
statement = wiz_id.button_export_pdf()
self.assertDictContainsSubset(
{
'type': 'ir.actions.report',
'report_name': self.report_name,
'report_type': 'qweb-pdf',
"type": "ir.actions.report",
"report_name": self.report_name,
"report_type": "qweb-pdf",
},
statement,
'There was an error and the PDF report was not generated.'
"There was an error and the PDF report was not generated.",
)
data = wiz_id._prepare_statement()
docids = data['partner_ids']
docids = data["partner_ids"]
report = self.statement_model._get_report_values(docids, data)
self.assertIsInstance(report, dict,
"There was an error while compiling the report.")
self.assertIn("bucket_labels", report,
"There was an error while compiling the report.")
self.assertIsInstance(
report, dict, "There was an error while compiling the report."
)
self.assertIn(
"bucket_labels", report, "There was an error while compiling the report."
)
def test_customer_activity_report_no_wizard(self):
docids = [self.partner1.id, self.partner2.id]
report = self.statement_model._get_report_values(docids, False)
self.assertIsInstance(report, dict,
"There was an error while compiling the report.")
self.assertIn("bucket_labels", report,
"There was an error while compiling the report.")
self.assertIsInstance(
report, dict, "There was an error while compiling the report."
)
self.assertIn(
"bucket_labels", report, "There was an error while compiling the report."
)
def test_date_formatting(self):
date_fmt = '%d/%m/%Y'
date_fmt = "%d/%m/%Y"
test_date = date(2018, 9, 30)
res = self.statement_model._format_date_to_partner_lang(
test_date, date_fmt
)
self.assertEqual(res, '30/09/2018')
res = self.statement_model._format_date_to_partner_lang(test_date, date_fmt)
self.assertEqual(res, "30/09/2018")
test_date_string = '2018-09-30'
test_date_string = "2018-09-30"
res = self.statement_model._format_date_to_partner_lang(
test_date_string, date_fmt
)
self.assertEqual(res, '30/09/2018')
self.assertEqual(res, "30/09/2018")
def test_onchange_aging_type(self):
"""Test that partner data is filled accodingly"""
wiz_id = self.wiz.with_context(
active_ids=[self.partner1.id, self.partner2.id],
active_ids=[self.partner1.id, self.partner2.id]
).new()
wiz_id.aging_type = 'months'
wiz_id.aging_type = "months"
wiz_id.onchange_aging_type()
self.assertEqual(wiz_id.date_end.month, wiz_id.date_start.month)
self.assertTrue(wiz_id.date_end.day > wiz_id.date_start.day)
self.assertTrue(wiz_id.date_end < self.today)
wiz_id.aging_type = 'days'
wiz_id.aging_type = "days"
wiz_id.onchange_aging_type()
self.assertEqual((wiz_id.date_end - wiz_id.date_start).days, 30)
self.assertTrue(wiz_id.date_end == self.today)

78
partner_statement/tests/test_outstanding_statement.py

@ -9,68 +9,74 @@ class TestOutstandingStatement(TransactionCase):
"""
Tests for Outstanding Statement.
"""
def setUp(self):
super().setUp()
self.res_users_model = self.env['res.users']
self.company = self.env.ref('base.main_company')
self.partner1 = self.env.ref('base.res_partner_2')
self.partner2 = self.env.ref('base.res_partner_3')
self.g_account_user = self.env.ref('account.group_account_user')
self.res_users_model = self.env["res.users"]
self.company = self.env.ref("base.main_company")
self.partner1 = self.env.ref("base.res_partner_2")
self.partner2 = self.env.ref("base.res_partner_3")
self.g_account_user = self.env.ref("account.group_account_user")
self.user = self._create_user('user_1', [self.g_account_user],
self.company).id
self.user = self._create_user("user_1", [self.g_account_user], self.company).id
self.statement_model = \
self.env['report.partner_statement.outstanding_statement']
self.wiz = self.env['outstanding.statement.wizard']
self.report_name = 'partner_statement.outstanding_statement'
self.report_title = 'Outstanding Statement'
self.statement_model = self.env[
"report.partner_statement.outstanding_statement"
]
self.wiz = self.env["outstanding.statement.wizard"]
self.report_name = "partner_statement.outstanding_statement"
self.report_title = "Outstanding Statement"
def _create_user(self, login, groups, company):
group_ids = [group.id for group in groups]
user = self.res_users_model.create({
'name': login,
'login': login,
'email': 'example@yourcompany.com',
'company_id': company.id,
'company_ids': [(4, company.id)],
'groups_id': [(6, 0, group_ids)]
})
user = self.res_users_model.create(
{
"name": login,
"login": login,
"email": "example@yourcompany.com",
"company_id": company.id,
"company_ids": [(4, company.id)],
"groups_id": [(6, 0, group_ids)],
}
)
return user
def test_customer_outstanding_statement(self):
wiz_id = self.wiz.with_context(
active_ids=[self.partner1.id, self.partner2.id],
active_ids=[self.partner1.id, self.partner2.id]
).create({})
wiz_id.aging_type = 'months'
wiz_id.aging_type = "months"
statement = wiz_id.button_export_pdf()
self.assertDictContainsSubset(
{
'type': 'ir.actions.report',
'report_name': self.report_name,
'report_type': 'qweb-pdf',
"type": "ir.actions.report",
"report_name": self.report_name,
"report_type": "qweb-pdf",
},
statement,
'There was an error and the PDF report was not generated.'
"There was an error and the PDF report was not generated.",
)
data = wiz_id._prepare_statement()
docids = data['partner_ids']
docids = data["partner_ids"]
report = self.statement_model._get_report_values(docids, data)
self.assertIsInstance(report,
dict,
"There was an error while compiling the report.")
self.assertIn("bucket_labels", report,
"There was an error while compiling the report.")
self.assertIsInstance(
report, dict, "There was an error while compiling the report."
)
self.assertIn(
"bucket_labels", report, "There was an error while compiling the report."
)
def test_customer_outstanding_report_no_wizard(self):
docids = [self.partner1.id]
report = self.statement_model._get_report_values(docids, False)
self.assertIsInstance(report, dict,
"There was an error while compiling the report.")
self.assertIn("bucket_labels", report,
"There was an error while compiling the report.")
self.assertIsInstance(
report, dict, "There was an error while compiling the report."
)
self.assertIn(
"bucket_labels", report, "There was an error while compiling the report."
)

31
partner_statement/tests/test_res_config_settings.py

@ -4,10 +4,9 @@ from odoo.tests.common import TransactionCase
class TestResConfigSettings(TransactionCase):
def setUp(self):
super().setUp()
self.config = self.env['res.config.settings']
self.config = self.env["res.config.settings"]
self.cr.execute(
"SELECT uid FROM res_groups_users_rel "
"WHERE gid IN (SELECT res_id FROM ir_model_data "
@ -15,22 +14,22 @@ class TestResConfigSettings(TransactionCase):
"ORDER BY uid DESC LIMIT 1"
)
self.account_user = self.cr.fetchone()[0]
self.user_obj = self.env['res.users'].sudo(self.account_user)
self.user_obj = self.env["res.users"].sudo(self.account_user)
def test_groups(self):
conf = self.config.create({
"default_aging_type": "months",
"group_activity_statement": True,
"group_outstanding_statement": False,
})
conf.set_values()
self.assertFalse(self.user_obj._has_group(
'partner_statement.group_outstanding_statement')
conf = self.config.create(
{
"default_aging_type": "months",
"group_activity_statement": True,
"group_outstanding_statement": False,
}
)
self.assertTrue(self.user_obj._has_group(
'partner_statement.group_activity_statement')
conf.set_values()
self.assertFalse(
self.user_obj._has_group("partner_statement.group_outstanding_statement")
)
res = self.env['ir.default'].get(
'activity.statement.wizard', 'aging_type'
self.assertTrue(
self.user_obj._has_group("partner_statement.group_activity_statement")
)
self.assertEquals(res, 'months')
res = self.env["ir.default"].get("activity.statement.wizard", "aging_type")
self.assertEquals(res, "months")

228
partner_statement/views/activity_statement.xml

@ -1,140 +1,172 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="partner_statement.activity_statement_document">
<t t-call="web.external_layout">
<t t-set="o" t-value="o.with_context({'lang': lang})"/>
<t t-set="o" t-value="o.with_context({'lang': lang})" />
<t t-set="address">
<address t-esc="get_inv_addr(o)" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
<div t-if="o.vat" class="mt16"><t t-esc="company.country_id.vat_label or 'Tax ID'"/>: <span t-field="o.vat"/></div>
<address
t-esc="get_inv_addr(o)"
t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'
/>
<div t-if="o.vat" class="mt16"><t
t-esc="company.country_id.vat_label or 'Tax ID'"
/>: <span t-field="o.vat" /></div>
</t>
<div class="page">
<h2 >Statement of Account</h2>
<h2>Statement of Account</h2>
<div id="informations" class="row mt32 mb32">
<div class="col-3 bm-2">
<strong>Date:</strong>
<p t-esc="d['today']" class="m-0"/>
<p t-esc="d['today']" class="m-0" />
</div>
<div t-if="o.ref" class="col-3 bm-2">
<strong>Partner Code:</strong>
<p t-field="o.ref" class="m-0" />
</div>
</div>
<t t-if="d['currencies']">
<br/>
<t t-foreach="d['currencies'].items()" t-as="currency">
<t t-set="display_currency" t-value="Currencies[currency[0]]" />
<t t-set="currency" t-value="currency[1]" />
<p>
<span t-if="account_type == 'payable'">Supplier </span>Statement between <span t-esc="d['start']" /> and <span t-esc="d['end']" /> in <span t-esc="display_currency.name"/>
<t t-if="d['currencies']">
<br />
<t t-foreach="d['currencies'].items()" t-as="currency">
<t t-set="display_currency" t-value="Currencies[currency[0]]" />
<t t-set="currency" t-value="currency[1]" />
<p>
<span
t-if="account_type == 'payable'"
>Supplier </span>Statement between <span
t-esc="d['start']"
/> and <span t-esc="d['end']" /> in <span
t-esc="display_currency.name"
/>
</p>
<table class="table table-condensed table-statement">
<thead>
<tr>
<th>Reference number</th>
<th>Date</th>
<th>Description</th>
<th class="amount">Amount</th>
<th class="amount">Balance</th>
</tr>
</thead>
<tbody>
<tr>
<td />
<td>
<span t-esc="d['start']"/>
</td>
<td>
<table class="table table-condensed table-statement">
<thead>
<tr>
<th>Reference number</th>
<th>Date</th>
<th>Description</th>
<th class="amount">Amount</th>
<th class="amount">Balance</th>
</tr>
</thead>
<tbody>
<tr>
<td />
<td>
<span t-esc="d['start']" />
</td>
<td>
Balance Forward
</td>
<td />
<td class="amount">
<span t-esc="currency['balance_forward']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
</tr>
<tr t-foreach="currency['lines']" t-as="line" t-att-class="'statement-blocked' if line['blocked'] else ''">
<td>
<span t-esc="line['move_id']"/>
</td>
<td>
<span t-esc="line['date']"/>
</td>
<td>
<t t-if="line['name'] != '/'">
<t t-if="not line['ref']">
<span t-esc="line['name']"/>
<td />
<td class="amount">
<span
t-esc="currency['balance_forward']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
</tr>
<tr
t-foreach="currency['lines']"
t-as="line"
t-att-class="'statement-blocked' if line['blocked'] else ''"
>
<td>
<span t-esc="line['move_id']" />
</td>
<td>
<span t-esc="line['date']" />
</td>
<td>
<t t-if="line['name'] != '/'">
<t t-if="not line['ref']">
<span t-esc="line['name']" />
</t>
<t t-if="line['ref'] and line['name']">
<t
t-if="line['name'] not in line['ref']"
>
<span t-esc="line['name']" />
</t>
<t t-if="line['ref'] and line['name']">
<t t-if="line['name'] not in line['ref']">
<span t-esc="line['name']"/>
</t>
<t t-if="line['ref'] not in line['name']">
<span t-esc="line['ref']"/>
</t>
<t t-if="line['name'] == line['ref']">
<span t-esc="line['name']"/>
</t>
<t
t-if="line['ref'] not in line['name']"
>
<span t-esc="line['ref']" />
</t>
<t t-if="line['name'] == line['ref']">
<span t-esc="line['name']" />
</t>
</t>
<t t-if="line['name'] == '/'">
<span t-if="line['ref'] == 'Payment'">Payment</span>
<span t-else="" t-esc="line['ref']"/>
</t>
</td>
<td class="amount">
<span t-esc="line['amount']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
<td class="amount">
<span t-esc="line['balance']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
</tr>
<tr>
<td />
<td>
<span t-esc="d['end']"/>
</td>
<td>
</t>
<t t-if="line['name'] == '/'">
<span
t-if="line['ref'] == 'Payment'"
>Payment</span>
<span t-else="" t-esc="line['ref']" />
</t>
</td>
<td class="amount">
<span
t-esc="line['amount']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span
t-esc="line['balance']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
</tr>
<tr>
<td />
<td>
<span t-esc="d['end']" />
</td>
<td>
Ending Balance
</td>
<td />
<td class="amount">
<span t-esc="currency['amount_due']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
</tr>
</tbody>
</table>
<t t-call="partner_statement.aging_buckets" t-if="currency['buckets']" />
</t>
<td />
<td class="amount">
<span
t-esc="currency['amount_due']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
</tr>
</tbody>
</table>
<t
t-call="partner_statement.aging_buckets"
t-if="currency['buckets']"
/>
</t>
<p t-if="d.get('no_entries')">
<strong>The partner doesn't have due entries.</strong>
</p>
</div>
</t>
<p t-if="d.get('no_entries')">
<strong>The partner doesn't have due entries.</strong>
</p>
</div>
</t>
</template>
<template id="activity_statement">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-set="d" t-value="data.get(o.id)" />
<t t-call="partner_statement.activity_statement_document" t-lang="o.lang"/>
<t
t-call="partner_statement.activity_statement_document"
t-lang="o.lang"
/>
</t>
</t>
</template>
<report id="action_print_activity_statement"
<report
id="action_print_activity_statement"
model="res.partner"
report_type="qweb-pdf"
menu="False"
string="Activity Statement"
name="partner_statement.activity_statement"
file="partner_statement.activity_statement"
/>
/>
</odoo>

43
partner_statement/views/aging_buckets.xml

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="aging_buckets">
<p>
Aging Report at <span t-esc="d['end']" /> in <span t-esc="display_currency.name"/>:
Aging Report at <span t-esc="d['end']" /> in <span
t-esc="display_currency.name"
/>:
</p>
<table class="table table-sm table-statement">
<thead>
@ -21,29 +22,49 @@
<tbody>
<tr>
<td class="amount">
<span t-esc="buckets.get('current', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('current', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span t-esc="buckets.get('b_1_30', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('b_1_30', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span t-esc="buckets.get('b_30_60', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('b_30_60', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span t-esc="buckets.get('b_60_90', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('b_60_90', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span t-esc="buckets.get('b_90_120', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('b_90_120', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span t-esc="buckets.get('b_over_120', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('b_over_120', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span t-esc="buckets.get('balance', 0.0)" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
<span
t-esc="buckets.get('balance', 0.0)"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
</tr>
</tbody>
</table>
</template>
</odoo>

16
partner_statement/views/assets.xml

@ -1,12 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="report_assets_common" name="oca_statements report assets" inherit_id="web.report_assets_common">
<template
id="report_assets_common"
name="oca_statements report assets"
inherit_id="web.report_assets_common"
>
<xpath expr="." position="inside">
<link rel="stylesheet" href="/partner_statement/static/src/scss/layout_statement.scss" type="text/scss" />
<link
rel="stylesheet"
href="/partner_statement/static/src/scss/layout_statement.scss"
type="text/scss"
/>
</xpath>
</template>
</odoo>

221
partner_statement/views/outstanding_statement.xml

@ -1,136 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="partner_statement.outstanding_statement_document">
<t t-call="web.external_layout">
<t t-set="o" t-value="o.with_context({'lang': lang})"/>
<t t-set="o" t-value="o.with_context({'lang': lang})" />
<t t-set="address">
<address t-esc="get_inv_addr(o)" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
<div t-if="o.vat" class="mt16"><t t-esc="company.country_id.vat_label or 'Tax ID'"/>: <span t-field="o.vat"/></div>
<address
t-esc="get_inv_addr(o)"
t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'
/>
<div t-if="o.vat" class="mt16"><t
t-esc="company.country_id.vat_label or 'Tax ID'"
/>: <span t-field="o.vat" /></div>
</t>
<div class="page">
<div class="oe_structure"/>
<h2 >Statement of Account</h2>
<div class="oe_structure" />
<h2>Statement of Account</h2>
<div id="informations" class="row mt32 mb32">
<div class="col-3 bm-2">
<strong>Date:</strong>
<p t-esc="d['today']" class="m-0"/>
<p t-esc="d['today']" class="m-0" />
</div>
<div t-if="o.ref" class="col-3 bm-2">
<strong>Partner Code:</strong>
<p t-field="o.ref" class="m-0" />
</div>
</div>
<t t-if="d['currencies']">
<br/>
<t t-foreach="d['currencies'].items()" t-as="currency">
<t t-set="display_currency" t-value="Currencies[currency[0]]" />
<t t-set="currency" t-value="currency[1]" />
<p>
<span t-esc="'' if account_type == 'receivable' else 'Supplier '"/>Statement up to <span t-esc="d['end']" /> in <span t-esc="display_currency.name"/>
<t t-if="d['currencies']">
<br />
<t t-foreach="d['currencies'].items()" t-as="currency">
<t t-set="display_currency" t-value="Currencies[currency[0]]" />
<t t-set="currency" t-value="currency[1]" />
<p>
<span
t-esc="'' if account_type == 'receivable' else 'Supplier '"
/>Statement up to <span t-esc="d['end']" /> in <span
t-esc="display_currency.name"
/>
</p>
<table class="table table-sm table-statement">
<thead>
<tr>
<th>Reference number</th>
<th>Date</th>
<th>Due Date</th>
<th>Description</th>
<th class="amount">Original</th>
<th class="amount">Open Amount</th>
<th class="amount">Balance</th>
</tr>
</thead>
<tbody>
<tr t-foreach="currency['lines']" t-as="line" t-att-class="'statement-blocked' if line['blocked'] else ''">
<td>
<span t-esc="line['move_id']"/>
</td>
<td>
<span t-esc="line['date']"/>
</td>
<td>
<span t-esc="line['date_maturity']"/>
</td>
<td>
<t t-if="line['name'] != '/'">
<t t-if="not line['ref']">
<span t-esc="line['name']"/>
<table class="table table-sm table-statement">
<thead>
<tr>
<th>Reference number</th>
<th>Date</th>
<th>Due Date</th>
<th>Description</th>
<th class="amount">Original</th>
<th class="amount">Open Amount</th>
<th class="amount">Balance</th>
</tr>
</thead>
<tbody>
<tr
t-foreach="currency['lines']"
t-as="line"
t-att-class="'statement-blocked' if line['blocked'] else ''"
>
<td>
<span t-esc="line['move_id']" />
</td>
<td>
<span t-esc="line['date']" />
</td>
<td>
<span t-esc="line['date_maturity']" />
</td>
<td>
<t t-if="line['name'] != '/'">
<t t-if="not line['ref']">
<span t-esc="line['name']" />
</t>
<t t-if="line['ref'] and line['name']">
<t
t-if="line['name'] not in line['ref']"
>
<span t-esc="line['name']" />
</t>
<t t-if="line['ref'] and line['name']">
<t t-if="line['name'] not in line['ref']">
<span t-esc="line['name']"/>
</t>
<t t-if="line['ref'] not in line['name']">
<span t-esc="line['ref']"/>
</t>
<t t-if="line['ref'] == line['name']">
<span t-esc="line['name']"/>
</t>
<t
t-if="line['ref'] not in line['name']"
>
<span t-esc="line['ref']" />
</t>
<t t-if="line['ref'] == line['name']">
<span t-esc="line['name']" />
</t>
</t>
<t t-if="line['name'] == '/'">
<span t-esc="line['ref']"/>
</t>
</td>
<td class="amount">
<span t-esc="line['amount']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
<td class="amount">
<span t-esc="line['open_amount']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
<td class="amount">
<span t-esc="line['balance']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
</tr>
<tr>
<td />
<td>
<span t-esc="d['end']"/>
</td>
<td>
</t>
<t t-if="line['name'] == '/'">
<span t-esc="line['ref']" />
</t>
</td>
<td class="amount">
<span
t-esc="line['amount']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span
t-esc="line['open_amount']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
<td class="amount">
<span
t-esc="line['balance']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
</tr>
<tr>
<td />
<td>
<span t-esc="d['end']" />
</td>
<td>
Ending Balance
</td>
<td />
<td />
<td />
<td class="amount">
<span t-esc="currency['amount_due']" t-options="{'widget': 'monetary', 'display_currency': display_currency}"/>
</td>
</tr>
</tbody>
</table>
<t t-call="partner_statement.aging_buckets" t-if="currency['buckets']" />
</t>
<td />
<td />
<td />
<td class="amount">
<span
t-esc="currency['amount_due']"
t-options="{'widget': 'monetary', 'display_currency': display_currency}"
/>
</td>
</tr>
</tbody>
</table>
<t
t-call="partner_statement.aging_buckets"
t-if="currency['buckets']"
/>
</t>
<p t-if="d.get('no_entries')">
<strong>The partner doesn't have due entries.</strong>
</p>
</div>
</t>
<p t-if="d.get('no_entries')">
<strong>The partner doesn't have due entries.</strong>
</p>
</div>
</t>
</template>
<template id="outstanding_statement">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-set="d" t-value="data.get(o.id)" />
<t t-call="partner_statement.outstanding_statement_document" t-lang="o.lang"/>
<t
t-call="partner_statement.outstanding_statement_document"
t-lang="o.lang"
/>
</t>
</t>
</template>
<report id="action_print_outstanding_statement"
<report
id="action_print_outstanding_statement"
model="res.partner"
report_type="qweb-pdf"
menu="False"
string="Outstanding Statement"
name="partner_statement.outstanding_statement"
file="partner_statement.outstanding_statement"
/>
/>
</odoo>

64
partner_statement/views/res_config_settings.xml

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<?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 (in partner_statement)</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="40"/>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="priority" eval="40" />
<field name="inherit_id" ref="account.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='account_followup']" position="after">
<div class="col-12 col-lg-6 o_setting_box">
@ -13,26 +12,50 @@
<field name="group_activity_statement" />
</div>
<div class="o_setting_right_pane">
<label for="group_activity_statement"/>
<label for="group_activity_statement" />
<div class="text-muted">
Activity Statements show all transactions between two dates.
</div>
<div class="content-group" attrs="{'invisible': [('group_activity_statement', '=', False), ('group_outstanding_statement', '=', False)]}">
<div
class="content-group"
attrs="{'invisible': [('group_activity_statement', '=', False), ('group_outstanding_statement', '=', False)]}"
>
<div class="row mt16">
<label for="default_aging_type" class="col-lg-3 o_light_label"/>
<label
for="default_aging_type"
class="col-lg-3 o_light_label"
/>
<field name="default_aging_type" />
</div>
<div >
<field name="default_show_aging_buckets" class="oe_inline" />
<label for="default_show_aging_buckets" class="o_light_label"/>
<div>
<field
name="default_show_aging_buckets"
class="oe_inline"
/>
<label
for="default_show_aging_buckets"
class="o_light_label"
/>
</div>
<div >
<field name="default_filter_partners_non_due" class="oe_inline" />
<label for="default_filter_partners_non_due" class="o_light_label"/>
<div>
<field
name="default_filter_partners_non_due"
class="oe_inline"
/>
<label
for="default_filter_partners_non_due"
class="o_light_label"
/>
</div>
<div >
<field name="default_filter_negative_balances" class="oe_inline" />
<label for="default_filter_negative_balances" class="o_light_label"/>
<div>
<field
name="default_filter_negative_balances"
class="oe_inline"
/>
<label
for="default_filter_negative_balances"
class="o_light_label"
/>
</div>
</div>
</div>
@ -40,14 +63,16 @@
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_outstanding_statement" />
</div>
<div class="o_setting_right_pane">
<label for="group_outstanding_statement"/>
<label for="group_outstanding_statement" />
<div class="text-muted">
Outstanding Statements show all transactions up to a date.
</div>
<div class="text-muted" attrs="{'invisible': [('group_outstanding_statement', '=', False)]}">
<div
class="text-muted"
attrs="{'invisible': [('group_outstanding_statement', '=', False)]}"
>
Please set defaults under Activity Statements.
</div>
</div>
@ -55,5 +80,4 @@
</xpath>
</field>
</record>
</odoo>

21
partner_statement/wizard/activity_statement_wizard.py

@ -3,29 +3,29 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class ActivityStatementWizard(models.TransientModel):
"""Activity Statement wizard."""
_inherit = 'statement.common.wizard'
_name = 'activity.statement.wizard'
_description = 'Activity Statement Wizard'
_inherit = "statement.common.wizard"
_name = "activity.statement.wizard"
_description = "Activity Statement Wizard"
@api.model
def _get_date_start(self):
return (
fields.Date.context_today(self).replace(day=1) -
relativedelta(days=1)
fields.Date.context_today(self).replace(day=1) - relativedelta(days=1)
).replace(day=1)
date_start = fields.Date(required=True, default=_get_date_start)
@api.onchange('aging_type')
@api.onchange("aging_type")
def onchange_aging_type(self):
super().onchange_aging_type()
if self.aging_type == 'months':
if self.aging_type == "months":
self.date_start = self.date_end.replace(day=1)
else:
self.date_start = self.date_end - relativedelta(days=30)
@ -34,11 +34,10 @@ class ActivityStatementWizard(models.TransientModel):
"""Export to PDF."""
data = self._prepare_statement()
return self.env.ref(
'partner_statement'
'.action_print_activity_statement').report_action(
self, data=data)
"partner_statement" ".action_print_activity_statement"
).report_action(self, data=data)
def _prepare_statement(self):
res = super()._prepare_statement()
res.update({'date_start': self.date_start})
res.update({"date_start": self.date_start})
return res

11
partner_statement/wizard/outstanding_statement_wizard.py

@ -8,14 +8,13 @@ from odoo import models
class OutstandingStatementWizard(models.TransientModel):
"""Outstanding Statement wizard."""
_name = 'outstanding.statement.wizard'
_inherit = 'statement.common.wizard'
_description = 'Outstanding Statement Wizard'
_name = "outstanding.statement.wizard"
_inherit = "statement.common.wizard"
_description = "Outstanding Statement Wizard"
def _export(self):
"""Export to PDF."""
data = self._prepare_statement()
return self.env.ref(
'partner_statement'
'.action_print_outstanding_statement').report_action(
self, data=data)
"partner_statement" ".action_print_outstanding_statement"
).report_action(self, data=data)

32
partner_statement/wizard/res_config_settings.py

@ -2,12 +2,12 @@ from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
_inherit = "res.config.settings"
group_activity_statement = fields.Boolean(
"Enable OCA Activity Statements",
group='account.group_account_invoice',
implied_group='partner_statement.group_activity_statement',
group="account.group_account_invoice",
implied_group="partner_statement.group_activity_statement",
)
default_aging_type = fields.Selection(
@ -19,40 +19,40 @@ class ResConfigSettings(models.TransientModel):
)
default_show_aging_buckets = fields.Boolean(
string="Show Aging Buckets",
default_model="statement.common.wizard",
string="Show Aging Buckets", default_model="statement.common.wizard"
)
default_filter_partners_non_due = fields.Boolean(
string='Exclude partners with no due entries',
string="Exclude partners with no due entries",
default_model="statement.common.wizard",
)
default_filter_negative_balances = fields.Boolean(
"Exclude Negative Balances",
default_model="statement.common.wizard",
"Exclude Negative Balances", default_model="statement.common.wizard"
)
group_outstanding_statement = fields.Boolean(
"Enable OCA Outstanding Statements",
group='account.group_account_invoice',
implied_group='partner_statement.group_outstanding_statement',
group="account.group_account_invoice",
implied_group="partner_statement.group_outstanding_statement",
)
def set_values(self):
self = self.with_context(active_test=False)
# default values fields
IrDefault = self.env['ir.default'].sudo()
IrDefault = self.env["ir.default"].sudo()
for name, field in self._fields.items():
if (name.startswith("default_") and
field.default_model == 'statement.common.wizard'):
if (
name.startswith("default_")
and field.default_model == "statement.common.wizard"
):
if isinstance(self[name], models.BaseModel):
if self._fields[name].type == 'many2one':
if self._fields[name].type == "many2one":
value = self[name].id
else:
value = self[name].ids
else:
value = self[name]
IrDefault.set('activity.statement.wizard', name[8:], value)
IrDefault.set('outstanding.statement.wizard', name[8:], value)
IrDefault.set("activity.statement.wizard", name[8:], value)
IrDefault.set("outstanding.statement.wizard", name[8:], value)
return super().set_values()

54
partner_statement/wizard/statement_common.py

@ -2,38 +2,37 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class StatementCommon(models.AbstractModel):
_name = 'statement.common.wizard'
_description = 'Statement Reports Common Wizard'
_name = "statement.common.wizard"
_description = "Statement Reports Common Wizard"
def _get_company(self):
return (
self.env['res.company'].browse(
self.env.context.get('force_company')) or
self.env.user.company_id
self.env["res.company"].browse(self.env.context.get("force_company"))
or self.env.user.company_id
)
name = fields.Char()
company_id = fields.Many2one(
comodel_name='res.company',
comodel_name="res.company",
default=_get_company,
string='Company',
string="Company",
required=True,
)
date_end = fields.Date(required=True, default=fields.Date.context_today)
show_aging_buckets = fields.Boolean(default=True)
number_partner_ids = fields.Integer(
default=lambda self: len(self._context['active_ids'])
default=lambda self: len(self._context["active_ids"])
)
filter_partners_non_due = fields.Boolean(
string="Don't show partners with no due entries", default=True)
filter_negative_balances = fields.Boolean(
"Exclude Negative Balances", default=True
string="Don't show partners with no due entries", default=True
)
filter_negative_balances = fields.Boolean("Exclude Negative Balances", default=True)
aging_type = fields.Selection(
[("days", "Age by Days"), ("months", "Age by Months")],
@ -43,16 +42,17 @@ class StatementCommon(models.AbstractModel):
)
account_type = fields.Selection(
[('receivable', 'Receivable'),
('payable', 'Payable')], string='Account type', default='receivable')
[("receivable", "Receivable"), ("payable", "Payable")],
string="Account type",
default="receivable",
)
@api.onchange('aging_type')
@api.onchange("aging_type")
def onchange_aging_type(self):
if self.aging_type == 'months':
self.date_end = (
fields.Date.context_today(self).replace(day=1) -
relativedelta(days=1)
)
if self.aging_type == "months":
self.date_end = fields.Date.context_today(self).replace(
day=1
) - relativedelta(days=1)
else:
self.date_end = fields.Date.context_today(self)
@ -64,14 +64,14 @@ class StatementCommon(models.AbstractModel):
def _prepare_statement(self):
self.ensure_one()
return {
'date_end': self.date_end,
'company_id': self.company_id.id,
'partner_ids': self._context['active_ids'],
'show_aging_buckets': self.show_aging_buckets,
'filter_non_due_partners': self.filter_partners_non_due,
'account_type': self.account_type,
'aging_type': self.aging_type,
'filter_negative_balances': self.filter_negative_balances,
"date_end": self.date_end,
"company_id": self.company_id.id,
"partner_ids": self._context["active_ids"],
"show_aging_buckets": self.show_aging_buckets,
"filter_non_due_partners": self.filter_partners_non_due,
"account_type": self.account_type,
"aging_type": self.aging_type,
"filter_negative_balances": self.filter_negative_balances,
}
def _export(self):

104
partner_statement/wizard/statement_wizard.xml

@ -1,24 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<!-- wizard action on res.partner -->
<act_window id="activity_statement_wizard_action"
<act_window
id="activity_statement_wizard_action"
name="Partner Activity Statement"
src_model="res.partner"
res_model="activity.statement.wizard"
view_type="form" view_mode="form"
key2="client_action_multi" target="new"
groups="partner_statement.group_activity_statement"/>
<act_window id="outstanding_statement_wizard_action"
view_type="form"
view_mode="form"
key2="client_action_multi"
target="new"
groups="partner_statement.group_activity_statement"
/>
<act_window
id="outstanding_statement_wizard_action"
name="Partner Outstanding Statement"
src_model="res.partner"
res_model="outstanding.statement.wizard"
view_type="form" view_mode="form"
key2="client_action_multi" target="new"
groups="partner_statement.group_outstanding_statement"/>
view_type="form"
view_mode="form"
key2="client_action_multi"
target="new"
groups="partner_statement.group_outstanding_statement"
/>
<!-- wizard view -->
<record id="statement_common_view" model="ir.ui.view">
<field name="name">Statement Common Wizard View</field>
@ -26,76 +32,98 @@
<field name="arch" type="xml">
<form name="Report Options">
<div style="text-align:justify" name="info">
<label string="Aging details can be shown in the report, expressed in aging
buckets, so the partner can review how much is open, due or overdue." for=""/>
</div><hr/>
<label
string="Aging details can be shown in the report, expressed in aging
buckets, so the partner can review how much is open, due or overdue."
for=""
/>
</div>
<hr />
<group>
<group name="main_info">
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<label for="account_type"/>
<field name="account_type" nolabel="1" widget="radio"/>
<field
name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"
/>
<label for="account_type" />
<field name="account_type" nolabel="1" widget="radio" />
</group>
<group name="aging_report">
<field name="show_aging_buckets"/>
<field name="show_aging_buckets" />
<field name="aging_type" />
</group>
</group>
<group>
<group name="dates">
<field name="date_end"/>
<field name="date_end" />
</group>
<group name="multiple_partners">
<field name="number_partner_ids" readonly="1" invisible="1"/>
<field name="filter_partners_non_due" attrs="{'invisible': [('number_partner_ids', '=', 1)]}" />
<field name="filter_negative_balances" attrs="{'invisible': [('number_partner_ids', '=', 1)]}" />
<field name="number_partner_ids" readonly="1" invisible="1" />
<field
name="filter_partners_non_due"
attrs="{'invisible': [('number_partner_ids', '=', 1)]}"
/>
<field
name="filter_negative_balances"
attrs="{'invisible': [('number_partner_ids', '=', 1)]}"
/>
</group>
</group>
<footer>
<button name="button_export_pdf" string="Export PDF" type="object" default_focus="1" class="oe_highlight"/>
<button
name="button_export_pdf"
string="Export PDF"
type="object"
default_focus="1"
class="oe_highlight"
/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="outstanding_statement_wizard_view" model="ir.ui.view">
<field name="name">Outstanding Statement Wizard</field>
<field name="model">outstanding.statement.wizard</field>
<field name="inherit_id" ref="partner_statement.statement_common_view" />
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='info']/label" position="before" >
<label string="The outstanding statement provides details of all partner's outstanding
<xpath expr="//div[@name='info']/label" position="before">
<label
string="The outstanding statement provides details of all partner's outstanding
receivables and payables up to a particular date. This includes all unpaid invoices, unclaimed
refunds and outstanding payments. The list is displayed in chronological order and is
split by currencies." for=""/>
<br/><br/>
split by currencies."
for=""
/>
<br />
<br />
</xpath>
</field>
</record>
<record id="activity_statement_wizard_view" model="ir.ui.view">
<field name="name">Activity Statement Wizard</field>
<field name="model">activity.statement.wizard</field>
<field name="inherit_id" ref="partner_statement.statement_common_view" />
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='info']/label" position="before" >
<label string="The activity statement provides details of all activity on
<xpath expr="//div[@name='info']/label" position="before">
<label
string="The activity statement provides details of all activity on
a partner's receivables and payables between two selected dates. This includes all invoices,
refunds and payments. Any outstanding balance dated prior to the chosen statement
period will appear as a forward balance at the top of the statement. The list is
displayed in chronological order and is split by currencies." for=""/>
<br/><br/>
displayed in chronological order and is split by currencies."
for=""
/>
<br />
<br />
</xpath>
<xpath expr="//field[@name='date_end']" position="before">
<field name="date_start"/>
<field name="date_start" />
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save