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 = '''
+
+
+
+ | RM Name |
+ Required Qty |
+ Available |
+ Shortage |
+
+
+
+ '''
+
+ 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'''
+
+ | {component.product_id.name} |
+ {required_qty:.2f} |
+ {available_qty:.2f} |
+
+ {shortage:.2f}
+ |
+
+ '''
+
+ html_table += '
'
+ 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 = '''
+
+
+
+
+ | Date |
+ Invoice |
+ Customer |
+ Product |
+ Quantity |
+ UOM |
+ Price |
+ Total |
+ Status |
+
+
+
+ '''
+
+ 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'''
+
+ | {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
+ |
+
+ '''
+
+ total_qty += quantity
+ total_amount += total
+
+ # Add totals row
+ html_table += f'''
+
+
+
+ | 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 = '''
+
+
+
+
+ | Production Order |
+ Product |
+ Planned Qty |
+ Produced Qty |
+ Start Date |
+ End Date |
+ Status |
+ Work Center |
+
+
+
+ '''
+
+ 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'''
+
+ | {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} |
+
+ '''
+
+ 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'''
+
+
+
+ | 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 @@
+
+
+
+
+
+
+
+
Customer Order:
+
+
+
+
Order Details
+
+
+ | Customer: |
+ |
+
+
+ | Product: |
+ |
+
+
+ | Quantity: |
+ |
+
+
+ | From Date: |
+ |
+
+
+ | To Date: |
+ |
+
+
+ | Status: |
+ |
+
+
+
+
+
+
Progress
+
+
+
+
+ | Ordered: |
+ |
+
+
+ | Delivered: |
+ |
+
+
+ | Remaining: |
+ |
+
+
+
+
+
+
+
+
+
+
+
Summary
+
+
+ | Total Orders in Report: |
+ |
+
+
+ | Total Quantity: |
+ |
+
+
+ | Total Delivered: |
+ |
+
+
+ | Total Pending: |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
+
+
+
+
+
+
+
+ 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