From f6381468240f15200fc52f79cee231a7fe1902e6 Mon Sep 17 00:00:00 2001 From: Raman Marikanti Date: Tue, 10 Feb 2026 12:47:18 +0530 Subject: [PATCH] customer orders Module --- custom_addons/customer_orders/__init__.py | 3 + custom_addons/customer_orders/__manifest__.py | 49 ++ .../customer_orders/controllers/__init__.py | 1 + .../customer_orders/controllers/main.py | 86 ++++ .../customer_orders/data/sequence.xml | 12 + .../customer_orders/models/__init__.py | 2 + .../models/customer_order_line.py | 243 +++++++++ .../customer_orders/models/customer_orders.py | 464 ++++++++++++++++++ .../customer_orders/reports/__init__.py | 1 + .../reports/customer_orders_report.py | 363 ++++++++++++++ .../reports/customer_orders_report.xml | 131 +++++ .../security/ir.model.access.csv | 3 + .../security/ir.model.access.xml | 17 + .../views/customer_orders_views.xml | 206 ++++++++ .../customer_orders/views/dashboard_views.xml | 28 ++ 15 files changed, 1609 insertions(+) create mode 100644 custom_addons/customer_orders/__init__.py create mode 100644 custom_addons/customer_orders/__manifest__.py create mode 100644 custom_addons/customer_orders/controllers/__init__.py create mode 100644 custom_addons/customer_orders/controllers/main.py create mode 100644 custom_addons/customer_orders/data/sequence.xml create mode 100644 custom_addons/customer_orders/models/__init__.py create mode 100644 custom_addons/customer_orders/models/customer_order_line.py create mode 100644 custom_addons/customer_orders/models/customer_orders.py create mode 100644 custom_addons/customer_orders/reports/__init__.py create mode 100644 custom_addons/customer_orders/reports/customer_orders_report.py create mode 100644 custom_addons/customer_orders/reports/customer_orders_report.xml create mode 100644 custom_addons/customer_orders/security/ir.model.access.csv create mode 100644 custom_addons/customer_orders/security/ir.model.access.xml create mode 100644 custom_addons/customer_orders/views/customer_orders_views.xml create mode 100644 custom_addons/customer_orders/views/dashboard_views.xml diff --git a/custom_addons/customer_orders/__init__.py b/custom_addons/customer_orders/__init__.py new file mode 100644 index 000000000..054ad7f23 --- /dev/null +++ b/custom_addons/customer_orders/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import reports +from . import controllers \ No newline at end of file diff --git a/custom_addons/customer_orders/__manifest__.py b/custom_addons/customer_orders/__manifest__.py new file mode 100644 index 000000000..11778dfeb --- /dev/null +++ b/custom_addons/customer_orders/__manifest__.py @@ -0,0 +1,49 @@ +{ + 'name': 'Customer Orders Management', + 'version': '18.0.1.0.0', + 'category': 'Sales', + 'summary': 'Manage customer orders with production and sales integration', + 'description': """ + Customer Orders Management Module + ================================= + + This module allows you to: + * Create and manage customer orders + * Track order progress from draft to delivery + * Create production orders automatically + * Generate sale orders from customer orders + * View comprehensive dashboard with analytics + * Generate detailed reports + + Features: + - Multi-stage workflow (Draft → Confirmed → Production → Delivery) + - Progress tracking with visual indicators + - Integration with Manufacturing and Sales modules + - Comprehensive dashboard with OWL JS + - Advanced reporting capabilities + """, + 'author': 'Raman Marikanti', + 'depends': ['base', 'mail', 'sale', 'mrp', 'stock','web_grid'], + 'data': [ + 'security/ir.model.access.csv', + 'security/ir.model.access.xml', + 'data/sequence.xml', + 'views/customer_orders_views.xml', + 'views/dashboard_views.xml', + 'reports/customer_orders_report.xml', + ], + 'assets': { + + 'web.assets_backend': [ + ('include', 'web_grid._assets_pqgrid'), + 'web/static/src/libs/fontawesome/*', + 'customer_orders/static/src/xml/dashboard.xml', + 'customer_orders/static/src/js/dashboard.js' + ], + }, + 'demo': [], + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/custom_addons/customer_orders/controllers/__init__.py b/custom_addons/customer_orders/controllers/__init__.py new file mode 100644 index 000000000..deec4a8b8 --- /dev/null +++ b/custom_addons/customer_orders/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/custom_addons/customer_orders/controllers/main.py b/custom_addons/customer_orders/controllers/main.py new file mode 100644 index 000000000..e986ba55a --- /dev/null +++ b/custom_addons/customer_orders/controllers/main.py @@ -0,0 +1,86 @@ +from odoo import http +from odoo.http import request +import json +from datetime import datetime, timedelta + + +class CustomerOrdersDashboard(http.Controller): + + @http.route('/customer_orders/dashboard_data', type='json', auth='user') + def dashboard_data(self, **kwargs): + env = request.env + + # Get all orders + orders = env['customer.order'].search([]) + + # Basic statistics + total_orders = len(orders) + total_quantity = sum(orders.mapped('quantity')) + delivered_quantity = sum(orders.mapped('delivered_qty')) + pending_quantity = total_quantity - delivered_quantity + + # Orders by state + orders_by_state = {} + for state_key, state_label in dict(env['customer.order']._fields['state'].selection).items(): + state_count = env['customer.orders'].search_count([('state', '=', state_key)]) + orders_by_state[state_key] = state_count + + # Monthly orders for last 6 months + monthly_orders = [] + # for i in range(5, -1, -1): + month_date = datetime.now() - timedelta(days=30) + month_start = month_date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + month_end = (month_start + timedelta(days=32)).replace(day=1) - timedelta(days=1) + + month_orders = env['customer.orders'].search([ + ('from_date', '>=', month_start), + ('from_date', '<=', month_end) + ]) + + monthly_orders.append({ + 'month': month_start.strftime('%b %Y'), + 'count': len(month_orders), + 'quantity': sum(month_orders.mapped('quantity')) + }) + + # Top customers + top_customers = [] + customer_orders = env['customer.orders'].read_group( + [('customer_id', '!=', False)], + ['customer_id:count'], + ['customer_id'] + ) + + for customer in sorted(customer_orders, key=lambda x: x['customer_id_count'], reverse=True)[:10]: + customer_record = env['res.partner'].browse(customer['customer_id'] if customer['customer_id'] else 1) + top_customers.append({ + 'id': customer_record.id, + 'name': customer_record.name, + 'order_count': customer['customer_id_count'] + }) + + # Top products + top_products = [] + product_orders = env['customer.orders'].read_group( + [('product_id', '!=', False)], + ['product_id', 'quantity:sum'], + ['product_id'] + ) + + for product in sorted(product_orders, key=lambda x: x['quantity'], reverse=True)[:10]: + product_record = env['product.product'].browse(product['product_id'][0]) + top_products.append({ + 'name': product_record.name, + 'total_quantity': product['quantity'] + }) + + return { + 'totalOrders': total_orders, + 'totalQuantity': total_quantity, + 'deliveredQuantity': delivered_quantity, + 'pendingQuantity': pending_quantity, + 'ordersByState': orders_by_state, + 'monthlyOrders': monthly_orders, + 'topCustomers': top_customers, + 'topProducts': top_products, + } \ No newline at end of file diff --git a/custom_addons/customer_orders/data/sequence.xml b/custom_addons/customer_orders/data/sequence.xml new file mode 100644 index 000000000..e653537d3 --- /dev/null +++ b/custom_addons/customer_orders/data/sequence.xml @@ -0,0 +1,12 @@ + + + + + Customer Orders + customer.orders + CO/%(year)s/ + 4 + + + + \ No newline at end of file diff --git a/custom_addons/customer_orders/models/__init__.py b/custom_addons/customer_orders/models/__init__.py new file mode 100644 index 000000000..5505c800d --- /dev/null +++ b/custom_addons/customer_orders/models/__init__.py @@ -0,0 +1,2 @@ +from . import customer_orders +from . import customer_order_line \ No newline at end of file diff --git a/custom_addons/customer_orders/models/customer_order_line.py b/custom_addons/customer_orders/models/customer_order_line.py new file mode 100644 index 000000000..17d24c4e9 --- /dev/null +++ b/custom_addons/customer_orders/models/customer_order_line.py @@ -0,0 +1,243 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError + + +class CustomerOrderLine(models.Model): + _name = 'customer.order.line' + _description = 'Customer Order Line' + _rec_name = 'product_id' + + order_id = fields.Many2one( + 'customer.order', + string='Order', + required=True, + ondelete='cascade', + index=True + ) + + product_id = fields.Many2one( + 'product.product', + string='Product', + required=True, + domain=[('type', '=', 'consu'),('sale_ok', '=', True)] + ) + + + + uom_id = fields.Many2one( + 'uom.uom', + string='UOM', + related='product_id.uom_id', + readonly=True, + store=True + ) + + order_qty = fields.Float( + string='Order Quantity', + required=True, + default=1.0, + digits='Product Unit of Measure' + ) + + produced_qty = fields.Float( + string='Produced Quantity', + compute='_compute_quantity', + store=True, + digits='Product Unit of Measure' + ) + customer_id = fields.Many2one(related="order_id.customer_id") + order_month = fields.Selection(related="order_id.order_month") + + dispatched_qty = fields.Float( + string='Dispatched Quantity', + compute='_compute_quantity', + store=True, + digits='Product Unit of Measure' + ) + + pending_production = fields.Float( + string='Pending Production', + compute='_compute_pending', + store=True, + digits='Product Unit of Measure' + ) + + pending_dispatch = fields.Float( + string='Pending Dispatch', + compute='_compute_pending', + store=True, + digits='Product Unit of Measure' + ) + + # BOM and RM fields + bom_id = fields.Many2one( + 'mrp.bom', + string='Bill of Material', + compute='_compute_bom', + store=True + ) + + rm_requirements = fields.Html( + string='RM Requirements', + compute='_compute_rm_requirements', + sanitize=False + ) + + rm_shortage = fields.Boolean( + string='RM Shortage', + compute='_compute_rm_shortage', + store=True + ) + + # Stock information + fg_available = fields.Float( + string='FG Available', + related='product_id.qty_available', + readonly=True + ) + + fg_shortage = fields.Float( + string='FG Shortage', + compute='_compute_fg_shortage', + store=True + ) + + def write(self, vals): + for line in self: + old_qty = line.order_qty + old_product = line.product_id.display_name + res = super().write(vals) + + if 'order_qty' in vals and line.order_id: + msg = ( + "Quantity for %s changed from %s to %s." + ) % ( + line.product_id.display_name, + old_qty, + vals['order_qty'] + ) + line.order_id.message_post(body=msg) + if 'product_id' in vals and line.order_id: + msg = ( + 'Product updated %s to %s .' + )%( old_product,line.product_id.display_name) + line.order_id.message_post(body=msg) + return res + + # @api.depends('order_id.dispatch_ids', 'order_id.dispatch_ids.state') + # def _compute_dispatch(self): + # """Compute dispatched quantity from dispatch entries""" + # for line in self: + # dispatches = self.env['daily.dispatch'].search([ + # ('order_id', '=', line.order_id.id), + # ('product_id', '=', line.product_id.id), + # ('state', '=', 'done') + # ]) + # line.dispatched_qty = sum(dispatches.mapped('quantity')) + # line.dispatched_qty = 0 + + @api.depends('order_qty', 'produced_qty', 'dispatched_qty') + def _compute_pending(self): + for line in self: + line.pending_production = max(0, line.order_qty - line.produced_qty) + line.pending_dispatch = max(0, line.produced_qty - line.dispatched_qty) + + @api.depends('order_qty', 'product_id', 'produced_qty', 'dispatched_qty', 'order_id.order_month', 'order_id.order_year') + def _compute_quantity(self): + for line in self: + + # One-liner + line.produced_qty = sum( + p.product_uom_qty + for p in self.env['mrp.production'].search([('product_id', '=', line.product_id.id),('state','=','done')]) + if p.date_start and p.date_start.month == int(line.order_id.order_month) and p.date_start.year == line.order_id.order_year + ) + line.dispatched_qty = sum( + self.env['account.move.line'].search([ + ('parent_state', '=', 'posted'), + ('product_id', '=', line.product_id.id), + ('move_id.move_type', 'in', ['out_invoice', 'out_refund']) + ]).filtered( + lambda l: l.move_id.invoice_date and + l.move_id.invoice_date.month == int(line.order_id.order_month) and + l.move_id.invoice_date.year == line.order_id.order_year + ).mapped('quantity') + ) + + + + @api.depends('product_id') + def _compute_bom(self): + for line in self: + bom = self.env['mrp.bom'].search([ + ('product_tmpl_id', '=', line.product_id.product_tmpl_id.id), + ('type', '=', 'normal') + ], limit=1) + line.bom_id = bom.id if bom else False + + def _compute_rm_requirements(self): + for line in self: + if line.bom_id: + html_table = ''' + + + + + + + + + + + ''' + + for component in line.bom_id.bom_line_ids: + required_qty = component.product_qty * line.order_qty + available_qty = component.product_id.qty_available + shortage = max(0, required_qty - available_qty) if component.product_id.type == "consu" else 0 + + html_table += f''' + + + + + + + ''' + + html_table += '
RM NameRequired QtyAvailableShortage
{component.product_id.name}{required_qty:.2f}{available_qty:.2f} + {shortage:.2f} +
' + line.rm_requirements = html_table + else: + line.rm_requirements = '
No BOM defined
' + + @api.depends('bom_id', 'order_qty') + def _compute_rm_shortage(self): + for line in self: + line.rm_shortage = False + if line.bom_id: + for component in line.bom_id.bom_line_ids.filtered(lambda x:x.product_id.type == "consu"): + required_qty = component.product_qty * line.order_qty + if component.product_id.qty_available < required_qty: + line.rm_shortage = True + break + + @api.depends('order_qty', 'fg_available') + def _compute_fg_shortage(self): + for line in self: + line.fg_shortage = max(0, line.order_qty - line.fg_available) + + def _calculate_rm_requirements(self): + """Calculate raw material requirements for this line""" + requirements = [] + if self.bom_id: + for component in self.bom_id.bom_line_ids: + requirements.append({ + 'product_id': component.product_id.id, + 'product_name': component.product_id.name, + 'quantity': component.product_qty * self.order_qty, + 'uom_id': component.product_uom_id.id, + 'available': component.product_id.qty_available, + }) + return requirements \ No newline at end of file diff --git a/custom_addons/customer_orders/models/customer_orders.py b/custom_addons/customer_orders/models/customer_orders.py new file mode 100644 index 000000000..694b79d69 --- /dev/null +++ b/custom_addons/customer_orders/models/customer_orders.py @@ -0,0 +1,464 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from datetime import datetime + + +class CustomerOrder(models.Model): + _name = 'customer.order' + _description = 'Customer Order' + _order = 'create_date desc' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char( + string='Order No', + required=True, + default='New', + readonly=True + ) + + order_month = fields.Selection( + selection=[ + ('1', 'January'), ('2', 'February'), ('3', 'March'), + ('4', 'April'), ('5', 'May'), ('6', 'June'), + ('7', 'July'), ('8', 'August'), ('9', 'September'), + ('10', 'October'), ('11', 'November'), ('12', 'December') + ], + string='Order Month', + required=True, + tracking=True + ) + + order_year = fields.Integer( + string='Order Year', + required=True, + default=fields.Date.today().year, + tracking=True + ) + + customer_id = fields.Many2one( + 'res.partner', + string='Customer', + required=True, + ) + + + + customer_location = fields.Char( + string='Location', + related='customer_id.city', + readonly=True + ) + + state = fields.Selection( + selection=[ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('in_progress', 'In Progress'), + ('partially', 'Partially Fulfilled'), + ('completed', 'Completed'), + ('cancelled', 'Cancelled') + ], + string='Status', + default='draft', + tracking=True + ) + + order_line_ids = fields.One2many( + 'customer.order.line', + 'order_id', + string='Order Lines', + copy=True, + ) + dispatch_data = fields.Html( + string='Dispatch data', + compute='_compute_dispatch_data', + sanitize=False + ) + + def _compute_dispatch_data(self): + for order in self: + # Get all invoice lines for this order + invoice_lines = self.env['account.move.line'].search([ + ('move_id.state', '=', 'posted'), + ('move_id.move_type', 'in', ['out_invoice', 'out_refund']), + ('product_id', 'in', order.order_line_ids.mapped('product_id').ids) + ]) + + if invoice_lines: + # Create HTML table + html_table = ''' +
+ + + + + + + + + + + + + + + + ''' + + total_qty = 0 + total_amount = 0 + + for line in invoice_lines.sorted(key=lambda l: l.move_id.invoice_date, reverse=True): + invoice = line.move_id + quantity = line.quantity + price = line.price_unit + total = line.price_subtotal + + html_table += f''' + + + + + + + + + + + + ''' + + total_qty += quantity + total_amount += total + + # Add totals row + html_table += f''' + + + + + + + + + + + +
DateInvoiceCustomerProductQuantityUOMPriceTotalStatus
{invoice.invoice_date.strftime('%d-%m-%Y') if invoice.invoice_date else ''}{invoice.name or ''}{invoice.partner_id.name or ''}{line.product_id.name or ''}{quantity:.2f}{line.product_uom_id.name or ''}₹{price:,.2f}₹{total:,.2f} + Posted +
Totals:{total_qty:.2f}₹{total_amount:,.2f}
+
+ +
+ + Total Dispatch: {total_qty:.2f} units | Total Amount: ₹{total_amount:,.2f} +
+ ''' + + order.dispatch_data = html_table + else: + order.dispatch_data = ''' +
+ + No dispatch invoices found for this order. +
+ ''' + # + production_data = fields.Html( + string='Production Data', + compute='_compute_production_data', + sanitize=False + ) + + def _compute_production_data(self): + for order in self: + # Get all production orders for this customer order's products + production_orders = self.env['mrp.production'].search([ + ('product_id', 'in', order.order_line_ids.mapped('product_id').ids), + ]) + + if production_orders: + # Create HTML table + html_table = ''' +
+ + + + + + + + + + + + + + + ''' + + total_planned = 0 + total_produced = 0 + + for prod in production_orders.sorted(key=lambda p: p.date_start or p.create_date, reverse=True): + planned_qty = prod.product_qty + produced_qty = prod.qty_produced + start_date = prod.date_start + end_date = prod.date_finished + + # Status badge + status_badge = '' + if prod.state == 'draft': + status_badge = 'Draft' + elif prod.state == 'confirmed': + status_badge = 'Confirmed' + elif prod.state == 'progress': + status_badge = 'In Progress' + elif prod.state == 'to_close': + status_badge = 'To Close' + elif prod.state == 'done': + status_badge = 'Done' + elif prod.state == 'cancel': + status_badge = 'Cancelled' + + # Work Center + work_center = prod.workorder_ids and prod.workorder_ids[0].workcenter_id.name or 'N/A' + + html_table += f''' + + + + + + + + + + + ''' + + total_planned += planned_qty + total_produced += produced_qty + + # Add totals row + efficiency = (total_produced / total_planned * 100) if total_planned > 0 else 0 + + html_table += f''' + + + + + + + + + + + +
Production OrderProductPlanned QtyProduced QtyStart DateEnd DateStatusWork Center
{prod.name}{prod.product_id.name}{planned_qty:.2f}{produced_qty:.2f}{start_date.strftime('%d-%m-%Y') if start_date else ''}{end_date.strftime('%d-%m-%Y') if end_date else ''}{status_badge}{work_center}
Totals:{total_planned:.2f}{total_produced:.2f}Efficiency: {efficiency:.1f}%
+
+ +
+ + Total Planned: {total_planned:.2f} | Total Produced: {total_produced:.2f} | + Balance: {total_planned - total_produced:.2f} +
+ ''' + + order.production_data = html_table + else: + order.production_data = ''' +
+ + No production orders found for this customer order. +
+ ''' + + # Computed fields + total_ordered_qty = fields.Float( + string='Total Ordered', + compute='_compute_totals', + store=True, + digits='Product Unit of Measure' + ) + + total_produced_qty = fields.Float( + string='Total Produced', + compute='_compute_totals', + store=True, + digits='Product Unit of Measure' + ) + + total_dispatched_qty = fields.Float( + string='Total Dispatched', + compute='_compute_totals', + store=True, + digits='Product Unit of Measure' + ) + + balance_qty = fields.Float( + string='Balance', + compute='_compute_totals', + store=True, + digits='Product Unit of Measure' + ) + + completion_percentage = fields.Float( + string='Completion %', + compute='_compute_completion', + store=True, + digits='Product Unit of Measure' + ) + + # Dates + order_date = fields.Date( + string='Order Date', + default=fields.Date.today(), + required=True + ) + + expected_completion_date = fields.Date( + string='Expected Completion' + ) + + actual_completion_date = fields.Date( + string='Actual Completion', + readonly=True + ) + + # Constraints + _sql_constraints = [ + ('unique_customer_month_year', + 'UNIQUE(customer_id, order_month, order_year)', + 'Only one order per customer per month is allowed!'), + ('check_order_year', + 'CHECK(order_year >= 2020 AND order_year <= 2100)', + 'Order year must be between 2020 and 2100!') + ] + + + + + @api.depends('order_line_ids.order_qty', 'order_line_ids.produced_qty', 'order_line_ids.dispatched_qty') + def _compute_totals(self): + for order in self: + lines = order.order_line_ids + order.total_ordered_qty = sum(lines.mapped('order_qty')) + order.total_produced_qty = sum(lines.mapped('produced_qty')) + order.total_dispatched_qty = sum(lines.mapped('dispatched_qty')) + order.balance_qty = order.total_ordered_qty - order.total_dispatched_qty + + @api.depends('total_dispatched_qty', 'total_ordered_qty') + def _compute_completion(self): + for order in self: + if order.total_ordered_qty > 0: + order.completion_percentage = (order.total_dispatched_qty / order.total_ordered_qty) * 100 + else: + order.completion_percentage = 0.0 + + @api.constrains('order_line_ids') + def _check_unique_products(self): + for order in self: + product_ids = [] + for line in order.order_line_ids: + if line.product_id.id in product_ids: + raise ValidationError( + _('Product %s appears more than once in order lines!') % line.product_id.name + ) + product_ids.append(line.product_id.id) + + @api.constrains('order_month', 'order_year') + def _check_future_month(self): + for order in self: + current_year = fields.Date.today().year + current_month = fields.Date.today().month + + if (order.order_year > current_year) or \ + (order.order_year == current_year and int(order.order_month) > current_month): + raise ValidationError(_('Cannot create orders for future months!')) + + def action_confirm(self): + for order in self: + order.state = 'confirmed' + # Create stock moves for raw materials if needed + if order.name == _('New'): + order.name = self.env['ir.sequence'].next_by_code('customer.orders') or _('New') + # order._create_rm_reservations() + + def action_complete(self): + for order in self: + if order.balance_qty == 0: + order.state = 'completed' + order.actual_completion_date = fields.Date.today() + else: + raise ValidationError(_('Cannot complete order with pending balance!')) + + def action_cancel(self): + for order in self: + order.state = 'cancelled' + + def get_dashboard_data(self, filters=None): + """Get data for dashboard""" + domain = [ + ('order_month', '=', str(filters.get('month'))), + ('order_year', '=', filters.get('year')) + ] + + if filters: + if filters.get('customer'): + domain.append(('customer_id', '=', int(filters['customer']))) + if filters.get('product'): + domain.append(('order_line_ids.product_id', '=', int(filters['product']))) + if filters.get('orderNo'): + domain.append(('name', 'ilike', filters['orderNo'])) + + orders = self.search(domain) + + return { + 'summary': self._prepare_summary(orders), + 'orders': self._prepare_orders_data(orders), + } + + def _prepare_summary(self, orders): + """Prepare summary statistics""" + return { + 'total_orders': len(orders), + 'total_ordered_qty': sum(orders.mapped('total_ordered_qty')), + 'total_produced_qty': sum(orders.mapped('total_produced_qty')), + 'total_dispatched_qty': sum(orders.mapped('total_dispatched_qty')), + 'balance_qty': sum(orders.mapped('balance_qty')), + 'completion_percentage': self._calculate_overall_completion(orders), + } + + def _prepare_orders_data(self, orders): + """Prepare orders data for frontend""" + data = [] + for order in orders: + for line in order.order_line_ids: + data.append({ + 'id': line.id, + 'customer_id': order.customer_id.id, + 'customer_name': order.customer_id.name, + 'order_no': order.name, + 'order_id': order.id, + 'product_id': line.product_id.id, + 'product_name': line.product_id.name, + 'ordered_qty': line.order_qty, + 'produced_qty': line.produced_qty, + 'dispatched_qty': line.dispatched_qty, + 'balance_qty': line.order_qty - line.dispatched_qty, + 'pending_production': line.pending_production, + 'pending_dispatch': line.pending_dispatch, + 'status': order.state, + 'fg_available': line.product_id.qty_available, + 'rm_shortage': line.rm_shortage, + }) + return data + + def _calculate_overall_completion(self, orders): + """Calculate overall completion percentage""" + total_ordered = sum(orders.mapped('total_ordered_qty')) + total_dispatched = sum(orders.mapped('total_dispatched_qty')) + if total_ordered > 0: + return (total_dispatched / total_ordered) * 100 + return 0.0 \ No newline at end of file diff --git a/custom_addons/customer_orders/reports/__init__.py b/custom_addons/customer_orders/reports/__init__.py new file mode 100644 index 000000000..e805b4aa2 --- /dev/null +++ b/custom_addons/customer_orders/reports/__init__.py @@ -0,0 +1 @@ +from . import customer_orders_report \ No newline at end of file diff --git a/custom_addons/customer_orders/reports/customer_orders_report.py b/custom_addons/customer_orders/reports/customer_orders_report.py new file mode 100644 index 000000000..e0fb1eae1 --- /dev/null +++ b/custom_addons/customer_orders/reports/customer_orders_report.py @@ -0,0 +1,363 @@ +from odoo import models, fields, api + + +class CustomerOrdersReport(models.AbstractModel): + _name = 'report.customer_orders.report_customer_orders' + _description = 'Customer Orders Report' + + @api.model + def _get_report_values(self, docids, data=None): + docs = self.env['customer.order'].browse(docids) + + return { + 'doc_ids': docids, + 'doc_model': 'customer.order', + 'docs': docs, + 'get_summary': self._get_summary, + 'get_monthly_data': self._get_monthly_data, + } + + def _get_summary(self, docs): + return { + 'total_orders': len(docs), + 'total_quantity': sum(docs.mapped('quantity')), + 'delivered_quantity': sum(docs.mapped('delivered_qty')), + 'pending_quantity': sum(docs.mapped('remaining_qty')), + } + + def _get_monthly_data(self, docs): + monthly_data = {} + for order in docs: + month_key = order.from_date.strftime('%Y-%m') + if month_key not in monthly_data: + monthly_data[month_key] = { + 'count': 0, + 'quantity': 0 + } + monthly_data[month_key]['count'] += 1 + monthly_data[month_key]['quantity'] += order.quantity + + return sorted(monthly_data.items()) + + +from odoo import models, fields, api +from datetime import datetime, timedelta +import json + + +class CORDashboard(models.AbstractModel): + _name = 'cor.dashboard' + _description = 'COR Dashboard' + + + @api.model + def get_customer_order_status_data(self,month,year): + month = str(int(month)) + year = str(int(year)) + sql = """ + SELECT + co.order_month AS month, + cu.complete_name AS customer, + CONCAT(pp.default_code, ' - ', pt.name->>'en_US') AS product, + cl.order_qty AS order_qty, + COALESCE(cl.order_qty * pp.weight, 0) as order_qty_kg, + COALESCE(fg.fg_qty, 0) AS fg_qty, + COALESCE(prod.produced_qty, 0) AS produced_qty, + COALESCE(prod.produced_qty* pp.weight, 0) AS produced_qty_kg, + COALESCE(dis.dispatched_qty, 0) AS dispatched_qty, + COALESCE(dis.dispatched_qty * pp.weight, 0) AS dispatched_qty_kg + FROM customer_order_line cl + + LEFT JOIN customer_order co + ON cl.order_id = co.id + + LEFT JOIN res_partner cu + ON co.customer_id = cu.id + + LEFT JOIN product_product pp + ON cl.product_id = pp.id + + LEFT JOIN product_template pt + ON pp.product_tmpl_id = pt.id + + -- 🔹 FG STOCK (current) + LEFT JOIN ( + SELECT + sq.product_id, + SUM(sq.quantity) AS fg_qty + FROM stock_quant sq + JOIN stock_location sl + ON sq.location_id = sl.id + WHERE sl.usage = 'internal' + GROUP BY sq.product_id + ) fg + ON fg.product_id = pp.id + + -- 🔹 PRODUCTION (month-wise) + LEFT JOIN ( + SELECT + DATE_TRUNC('month', mp.date_start)::date AS month, + mp.product_id, + SUM(mp.product_qty) AS produced_qty + FROM mrp_production mp + WHERE mp.state = 'done' + GROUP BY + DATE_TRUNC('month', mp.date_start), + mp.product_id + ) prod + ON prod.product_id = pp.id + AND EXTRACT(MONTH FROM prod.month) = co.order_month::int + + -- 🔹 DISPATCHED QTY (Invoices) + LEFT JOIN ( + SELECT + DATE_TRUNC('month', am.invoice_date)::date AS month, + aml.product_id, + SUM(aml.quantity) AS dispatched_qty + FROM account_move_line aml + JOIN account_move am + ON aml.move_id = am.id + WHERE am.state = 'posted' + GROUP BY + DATE_TRUNC('month', am.invoice_date), + aml.product_id + ) dis + ON dis.product_id = pp.id + AND EXTRACT(MONTH FROM dis.month) = co.order_month::int + WHERE co.order_month = %s AND co.order_year = %s + ORDER BY + co.order_month, + cu.complete_name; + + """ + params = (month, year) + self.env.cr.execute(sql, params) + + data = self.env.cr.dictfetchall() + return data or [] + + @api.model + def get_raw_material_availability_data(self,year,month): + try: + month = str(int(month)) + year = str(int(year)) + sql = """ + SELECT + concat(rpp.default_code , ' - ',rpt.name->>'en_US') as product, + uu.name->>'en_US' as uom_name, + SUM(col.order_qty * boml.product_qty) as required_qty, + COALESCE(sq.quantity, 0) as available_qty, + CASE + WHEN SUM(col.order_qty * boml.product_qty) - COALESCE(sq.quantity, 0) > 0 + THEN SUM(col.order_qty * boml.product_qty) - COALESCE(sq.quantity, 0) + ELSE 0 + END AS shortage_qty + FROM customer_order_line col + LEFT JOIN customer_order co + ON col.order_id = co.id + INNER JOIN product_product pp ON pp.id = col.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN mrp_bom bom ON bom.product_tmpl_id = pt.id AND bom.active = true + INNER JOIN mrp_bom_line boml ON boml.bom_id = bom.id + INNER JOIN product_product rpp ON rpp.id = boml.product_id + INNER JOIN product_template rpt ON rpt.id = rpp.product_tmpl_id + LEFT JOIN uom_uom uu ON uu.id = boml.product_uom_id + LEFT JOIN ( + SELECT + product_id, + SUM(quantity) as quantity + FROM stock_quant + WHERE location_id IN ( + SELECT id FROM stock_location WHERE usage = 'internal' + ) + GROUP BY product_id + ) sq ON sq.product_id = boml.product_id + WHERE + col.order_qty > 0 AND rpt.type = 'consu' AND co.order_month = %s AND co.order_year = %s + GROUP BY + boml.product_id, + rpp.default_code, + rpt.name, + uu.name, + sq.quantity + HAVING SUM(col.order_qty * boml.product_qty) > 0 + ORDER BY required_qty DESC; + """ + params = (month, year) + self.env.cr.execute(sql, params) + data = self.env.cr.dictfetchall() + return data or [] + except Exception as e: + return [] + + @api.model + def get_dispatch_summary_data(self, month,year): + try: + month = str(int(month)) + year = str(int(year)) + query = """ + SELECT + CONCAT(pp.default_code, ' : ', pt.name->>'en_US') AS product, + uu.name->>'en_US' AS uom_name, + co.order_month AS month, + cu.complete_name AS customer, + SUM(aml.quantity) AS invoiced_qty, + SUM(aml.quantity * pp.weight) AS invoiced_qty_kg, + col.order_qty AS order_qty, + col.order_qty * pp.weight AS order_qty_kg, + json_agg( + json_build_object( + 'invoice_date', aml.invoice_date, + 'qty', aml.quantity + ) + ORDER BY aml.invoice_date + ) FILTER (WHERE aml.invoice_date IS NOT NULL) AS invoice_date_qty + + -- aml.invoice_date as invoice_date + + FROM customer_order_line col + + JOIN product_product pp + ON pp.id = col.product_id + + JOIN product_template pt + ON pt.id = pp.product_tmpl_id + + LEFT JOIN uom_uom uu + ON uu.id = pt.uom_id + LEFT JOIN customer_order co + ON col.order_id = co.id + + LEFT JOIN account_move_line aml + ON aml.product_id = pp.id + AND aml.invoice_date >= make_date(co.order_year, co.order_month::int, 1) + AND aml.invoice_date < make_date(co.order_year, co.order_month::int, 1) + interval '1 month' + + + LEFT JOIN res_partner cu + ON co.customer_id = cu.id + + WHERE col.order_qty > 0 AND co.order_month = %s AND co.order_year = %s + + GROUP BY + pp.default_code, + pt.name, + uu.name, + pp.weight, + co.order_month, + cu.complete_name, + col.order_qty + """ + + params = (month, year) + self.env.cr.execute(query, params) + results = self.env.cr.dictfetchall() + return results + + except Exception as e: + return [] + + +# Add this method to your cor.dashboard model + + def get_dpr_daily_production_data(self): + """Get DPR (Daily Production Report) data""" + try: + # Get current date + today = fields.Date.today() + first_day_of_month = today.replace(day=1) + last_day_of_prev_month = first_day_of_month - timedelta(days=1) + first_day_of_prev_month = last_day_of_prev_month.replace(day=1) + + # Initialize result + result = [] + + # Get all products + products = self.env['product.product'].search([ + ('type', '=', 'product'), + ('categ_id.complete_name', 'ilike', 'Finished Goods') + ]) + + for product in products: + # Get pending orders from last month + pending_orders = self.env['sale.order.line'].search([ + ('product_id', '=', product.id), + ('order_id.state', 'in', ['sale', 'done']), + ('order_id.date_order', '>=', first_day_of_prev_month), + ('order_id.date_order', '<=', last_day_of_prev_month), + ('qty_delivered', '<', 'product_uom_qty') + ]) + + pending_qty = sum(pending_orders.mapped('product_uom_qty')) - sum(pending_orders.mapped('qty_delivered')) + pending_value = pending_qty * product.standard_price + + # Get present month orders + present_orders = self.env['sale.order.line'].search([ + ('product_id', '=', product.id), + ('order_id.state', 'in', ['sale', 'done']), + ('order_id.date_order', '>=', first_day_of_month), + ('order_id.date_order', '<=', today) + ]) + + present_qty = sum(present_orders.mapped('product_uom_qty')) + present_value = present_qty * product.standard_price + + # Get FG available quantity + fg_qty = product.qty_available + fg_value = fg_qty * product.standard_price + + # Get dispatch qty for current month + moves = self.env['stock.move'].search([ + ('product_id', '=', product.id), + ('state', '=', 'done'), + ('date', '>=', first_day_of_month), + ('date', '<=', today), + ('location_dest_id.usage', '=', 'customer'), + ('location_id.usage', '=', 'internal') + ]) + + dispatch_qty = sum(moves.mapped('quantity_done')) + dispatch_value = dispatch_qty * product.standard_price + + # Get daily production for last 31 days + daily_data = {} + for i in range(31, 0, -1): + day_date = today - timedelta(days=(31 - i)) + day_moves = self.env['stock.move'].search([ + ('product_id', '=', product.id), + ('state', '=', 'done'), + ('date', '>=', day_date), + ('date', '<', day_date + timedelta(days=1)), + ('production_id', '!=', False) # Production moves + ]) + daily_data[f'day{i}_qty'] = sum(day_moves.mapped('quantity_done')) + + # Calculate totals + total_order_qty = pending_qty + present_qty + total_order_value = pending_value + present_value + remaining_qty = max(0, total_order_qty - fg_qty) + remaining_value = max(0, total_order_value - fg_value) + + result.append({ + 'product_id': product.id, + 'product_name': product.display_name, + 'pending_order_qty': pending_qty, + 'pending_order_value': pending_value, + 'present_month_order_qty': present_qty, + 'present_month_order_value': present_value, + 'total_order_qty': total_order_qty, + 'total_order_value': total_order_value, + 'fg_available_qty': fg_qty, + 'fg_available_value': fg_value, + 'dispatch_qty': dispatch_qty, + 'dispatch_value': dispatch_value, + 'remaining_to_produce_qty': remaining_qty, + 'remaining_to_produce_value': remaining_value, + **daily_data + }) + + return result + + except Exception as e: + # _logger.error(f"Error in get_dpr_daily_production_data: {str(e)}") + return [] \ No newline at end of file diff --git a/custom_addons/customer_orders/reports/customer_orders_report.xml b/custom_addons/customer_orders/reports/customer_orders_report.xml new file mode 100644 index 000000000..080316997 --- /dev/null +++ b/custom_addons/customer_orders/reports/customer_orders_report.xml @@ -0,0 +1,131 @@ + + + + + + \ No newline at end of file diff --git a/custom_addons/customer_orders/security/ir.model.access.csv b/custom_addons/customer_orders/security/ir.model.access.csv new file mode 100644 index 000000000..840759fb9 --- /dev/null +++ b/custom_addons/customer_orders/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_customer_orders,customer.order,model_customer_order,base.group_user,1,1,1,1 +access_customer_order_line,customer.order.line,model_customer_order_line,base.group_user,1,1,1,1 diff --git a/custom_addons/customer_orders/security/ir.model.access.xml b/custom_addons/customer_orders/security/ir.model.access.xml new file mode 100644 index 000000000..e4471ec74 --- /dev/null +++ b/custom_addons/customer_orders/security/ir.model.access.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + Customer Orders + + + + + \ No newline at end of file diff --git a/custom_addons/customer_orders/views/customer_orders_views.xml b/custom_addons/customer_orders/views/customer_orders_views.xml new file mode 100644 index 000000000..478e1b733 --- /dev/null +++ b/custom_addons/customer_orders/views/customer_orders_views.xml @@ -0,0 +1,206 @@ + + + + + + + + customer.order.form + customer.order + +
+
+
+ +
+ +
+
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Business Rules: +
    +
  • One order per customer per month
  • +
  • Unique product per order
  • +
  • Monthly tracking of all quantities
  • +
+
+
+
+
+ + +
+
+ + + + customer.order.list + customer.order + + + + + + + + + + + + + + + + + customer.order.search + customer.order + + + + + + + + + + + + + + + + + + + + + + customer.order.report.pivot + customer.order + + + + + + + + + + + + + + customer.order.line.pivot + customer.order.line + + + + + + + + + + + + + + Customer Orders + customer.order.line + pivot + +

+ Create your first customer order +

+
+
+ + + + Customer Orders + customer.order + list,form,pivot + + +

+ Create your first customer order +

+
+
+ + + + + +
+
\ No newline at end of file diff --git a/custom_addons/customer_orders/views/dashboard_views.xml b/custom_addons/customer_orders/views/dashboard_views.xml new file mode 100644 index 000000000..725766bbd --- /dev/null +++ b/custom_addons/customer_orders/views/dashboard_views.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + Overview + CustomerOrderStatusGrid + + + + \ No newline at end of file