diff --git a/product_cores_report/__init__.py b/product_cores_report/__init__.py new file mode 100644 index 00000000..09434554 --- /dev/null +++ b/product_cores_report/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models diff --git a/product_cores_report/__manifest__.py b/product_cores_report/__manifest__.py new file mode 100644 index 00000000..06967d35 --- /dev/null +++ b/product_cores_report/__manifest__.py @@ -0,0 +1,21 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Product Core Reporting', + 'version': '13.0.1.0.0', + 'category': 'Account', + 'author': 'Hibou Corp.', + 'license': 'OPL-1', + 'website': 'https://hibou.io/', + 'depends': [ + 'account_reports', + 'product_cores', + ], + 'data': [ + 'views/account_views.xml', + 'views/report_templates.xml', + ], + 'installable': True, + 'auto_install': True, + 'application': False, + } diff --git a/product_cores_report/models/__init__.py b/product_cores_report/models/__init__.py new file mode 100644 index 00000000..c957b3cd --- /dev/null +++ b/product_cores_report/models/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import product_core_aged diff --git a/product_cores_report/models/product_core_aged.py b/product_cores_report/models/product_core_aged.py new file mode 100644 index 00000000..de4c4b0b --- /dev/null +++ b/product_cores_report/models/product_core_aged.py @@ -0,0 +1,211 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import models, api, fields, _ +from odoo.tools.misc import format_date + + +class ProductCoreAgedReport(models.AbstractModel): + _name = 'product.core.aged.report' + _description = 'Aged Product Cores' + _inherit = 'account.report' + + filter_date = {'mode': 'single', 'filter': 'today'} + filter_unfold_all = False + filter_partner = True + order_selected_column = {'default': 0} + + def _get_columns_name(self, options): + columns = [ + {}, + {'name': _('Partner'), 'class': '', 'style': 'white-space:nowrap;'}, + {'name': _('Date'), 'class': 'date', 'style': 'white-space:nowrap;'}, + {'name': _('Exp. Date'), 'class': 'date', 'style': 'white-space:nowrap;'}, + {'name': _('Account'), 'class': '', 'style': 'text-align:center; white-space:nowrap;'}, + {'name': _("As of: %s") % format_date(self.env, options['date']['date_to']), 'class': 'number sortable', 'style': 'white-space:nowrap;'}, + {'name': _("Qty."), 'class': 'number sortable', 'style': 'white-space:nowrap;'}, + {'name': _("Expired"), 'class': 'number sortable', 'style': 'white-space:nowrap;'}, + {'name': _("Exp. Qty."), 'class': 'number sortable', 'style': 'white-space:nowrap;'}, + ] + return columns + + def _get_templates(self): + templates = super(ProductCoreAgedReport, self)._get_templates() + templates['main_template'] = 'product_cores_report.template_product_core_aged_report' + templates['line_template'] = 'product_cores_report.template_product_core_aged_line_report' + return templates + + @api.model + def _get_lines(self, options, line_id=None): + # We may need to reverse the sign at some point in the future + sign = 1.0 + ctx = self._context + lines = [] + cr = self.env.cr + user_company = self.env.company + company_ids = self._context.get('company_ids') or [user_company.id] + + # expand line, filter just for this product + product = False + if line_id and 'product_' in line_id: + # we only want to fetch data about this product because we are expanding a line + product_id_str = line_id.split('_')[1] + if product_id_str.isnumeric(): + product = self.env['product.product'].browse(int(product_id_str)) + + date_from = fields.Date.from_string(self._context['date_to']) + + # put in to constants CTE for easier query + arg_list = (date_from, ) + + # product filtering + product_clause = '' + if product: + product_clause = 'AND (pp.id IN %s)' + arg_list += (tuple(product.ids), ) + + # partner filtering + partner_clause = '' + if 'partner_ids' in ctx: + if ctx['partner_ids']: + partner_clause = 'AND (l.partner_id IN %s)' + arg_list += (tuple(ctx['partner_ids'].ids),) + else: + partner_clause = 'AND l.partner_id IS NULL' + if ctx.get('partner_categories'): + partner_clause += 'AND (l.partner_id IN %s)' + partner_ids = self.env['res.partner'].search([('category_id', 'in', ctx['partner_categories'].ids)]).ids + arg_list += (tuple(partner_ids or [0]),) + + query = ''' + WITH + constants (date_from) AS (VALUES (%s)), + product_cores AS ( + SELECT pp.id AS id, pt.product_core_validity AS product_core_validity, pt.name AS product_name + FROM product_template AS pt + INNER JOIN product_product AS pp ON pp.product_tmpl_id = pt.id + WHERE pt.core_ok = true + AND pt.type = 'service' + ''' + product_clause + ''' + ) + SELECT pc.id AS product_id, + MAX(pc.product_name) AS product_name, + MAX(UPPER(pc.product_name)) AS UPNAME, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) < (SELECT date_from FROM constants) AND l.debit != 0 THEN l.quantity + WHEN COALESCE(l.date_maturity, l.date) < (SELECT date_from FROM constants) THEN -l.quantity + ELSE 0.0 END) AS total_expired_qty, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) < (SELECT date_from FROM constants) THEN l.debit + ELSE 0.0 END) AS total_expired_debit, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) < (SELECT date_from FROM constants) THEN l.credit + ELSE 0.0 END) AS total_expired_credit, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) >= (SELECT date_from FROM constants) AND l.debit != 0 THEN l.quantity + WHEN COALESCE(l.date_maturity, l.date) >= (SELECT date_from FROM constants) THEN -l.quantity + ELSE 0.0 END) AS total_qty, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) >= (SELECT date_from FROM constants) THEN l.debit + ELSE 0.0 END) AS total_debit, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) >= (SELECT date_from FROM constants) THEN l.credit + ELSE 0.0 END) AS total_credit, + SUM(CASE + WHEN COALESCE(l.date_maturity, l.date) >= (SELECT date_from FROM constants) THEN l.credit + ELSE 0.0 END) AS total_credit, + array_agg(l.id) FILTER (WHERE COALESCE(l.date_maturity, l.date) >= (SELECT date_from FROM constants)) AS aml_ids + FROM account_move_line AS l + INNER JOIN product_cores AS pc ON l.product_id = pc.id + WHERE (l.date <= (SELECT date_from FROM constants)) + ''' + partner_clause + ''' + AND (l.company_id IN %s) + GROUP BY pc.id + ORDER BY UPNAME ASC + ''' + arg_list += (tuple(company_ids), ) + cr.execute(query, arg_list) + + totals = {'total': 0.0, 'total_qty': 0.0, 'total_expired': 0.0, 'total_expired_qty': 0.0} + product_cores = cr.dictfetchall() + for product_core in product_cores: + ref = 'product_%s' % (product_core['product_id'], ) + product_total = product_core['total_debit'] - product_core['total_credit'] + product_total_qty = product_core['total_qty'] + product_expired_total = product_core['total_expired_debit'] - product_core['total_expired_credit'] + product_expired_total_qty = product_core['total_expired_qty'] + totals['total'] += product_total + totals['total_qty'] += product_total_qty + totals['total_expired'] += product_expired_total + totals['total_expired_qty'] += product_expired_total_qty + vals = { + 'id': ref, + 'name': product_core['product_name'], + 'level': 2, + 'columns': [{'name': ''}] * 4 + + [{'name': self.format_value(sign * product_total), 'no_format': sign * product_total}, + {'name': sign * product_total_qty, 'no_format': sign * product_total_qty}, + {'name': self.format_value(sign * product_expired_total), 'no_format': sign * product_expired_total}, + {'name': sign * product_expired_total_qty, 'no_format': sign * product_expired_total_qty}, + ], + 'unfoldable': True, + 'unfolded': ref in options.get('unfolded_lines'), + } + lines.append(vals) + if ref in options.get('unfolded_lines'): + amls = self.env['account.move.line'].browse(product_core['aml_ids']) + for aml in amls: + if aml.move_id.is_purchase_document(): + caret_type = 'account.invoice.in' + elif aml.move_id.is_sale_document(): + caret_type = 'account.invoice.out' + elif aml.payment_id: + caret_type = 'account.payment' + else: + caret_type = 'account.move' + + expires = aml.date_maturity if aml.date_maturity else aml.date + expired = expires < date_from + amount = aml.debit - aml.credit + amount_not_expired = amount if not expired else 0.0 + amount_expired = amount if expired else 0.0 + qty = aml.quantity if aml.debit else -aml.quantity + qty_not_expired = qty if not expired else 0.0 + qty_expired = qty if expired else 0.0 + + vals = { + 'id': aml.id, + 'name': aml.move_id.name, + 'class': 'date', + 'caret_options': caret_type, + 'level': 4, + 'parent_id': ref, + 'columns': [{'name': v} for v in [aml.partner_id.display_name or 'Undefined', format_date(self.env, aml.date), format_date(self.env, expires), aml.account_id.display_name]] + + [{'name': self.format_value(sign * amount_not_expired, blank_if_zero=True), 'no_format': sign * amount_not_expired}, + {'name': sign * qty_not_expired or '', 'no_format': sign * qty_not_expired}, + {'name': self.format_value(sign * amount_expired, blank_if_zero=True), 'no_format': sign * amount_expired}, + {'name': sign * qty_expired or '', 'no_format': sign * qty_expired}, + ], + 'action_context': { + 'default_type': aml.move_id.type, + 'default_journal_id': aml.move_id.journal_id.id, + }, + 'title_hover': self._format_aml_name(aml.name, aml.ref, aml.move_id.name), + } + lines.append(vals) + + if not line_id: + total_line = { + 'id': 0, + 'name': _('Total'), + 'class': 'total', + 'level': 2, + 'columns': [{'name': ''}] * 4 + + [{'name': self.format_value(sign * totals['total']), 'no_format': sign * totals['total']}, + {'name': sign * totals['total_qty'], 'no_format': sign * totals['total_qty']}, + {'name': self.format_value(sign * totals['total_expired']), 'no_format': sign * totals['total_expired']}, + {'name': sign * totals['total_expired_qty'], 'no_format': sign * totals['total_expired_qty']}, + ], + } + lines.append(total_line) + + return lines diff --git a/product_cores_report/views/account_views.xml b/product_cores_report/views/account_views.xml new file mode 100644 index 00000000..14ea69fc --- /dev/null +++ b/product_cores_report/views/account_views.xml @@ -0,0 +1,13 @@ + + + + + Product Cores + account_report + + + + + + \ No newline at end of file diff --git a/product_cores_report/views/report_templates.xml b/product_cores_report/views/report_templates.xml new file mode 100644 index 00000000..a4253449 --- /dev/null +++ b/product_cores_report/views/report_templates.xml @@ -0,0 +1,12 @@ + + + + + +