customer orders Module

This commit is contained in:
Raman Marikanti 2026-02-10 12:47:18 +05:30
parent fe335edd7c
commit f638146824
15 changed files with 1609 additions and 0 deletions

View File

@ -0,0 +1,3 @@
from . import models
from . import reports
from . import controllers

View File

@ -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',
}

View File

@ -0,0 +1 @@
from . import main

View File

@ -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,
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_customer_orders" model="ir.sequence">
<field name="name">Customer Orders</field>
<field name="code">customer.orders</field>
<field name="prefix">CO/%(year)s/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,2 @@
from . import customer_orders
from . import customer_order_line

View File

@ -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 <b>%s</b> changed from <b>%s</b> to <b>%s</b>."
) % (
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 <b> %s</b> to <b> %s </b>.'
)%( 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 = '''
<table class="table table-bordered" style="width:100%">
<thead>
<tr>
<th>RM Name</th>
<th>Required Qty</th>
<th>Available</th>
<th>Shortage</th>
</tr>
</thead>
<tbody>
'''
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'''
<tr>
<td>{component.product_id.name}</td>
<td>{required_qty:.2f}</td>
<td>{available_qty:.2f}</td>
<td style="color: {'red' if shortage > 0 else 'green'}">
{shortage:.2f}
</td>
</tr>
'''
html_table += '</tbody></table>'
line.rm_requirements = html_table
else:
line.rm_requirements = '<div class="alert alert-info">No BOM defined</div>'
@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

View File

@ -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 = '''
<div class="table-responsive">
<table class="table table-bordered table-sm" style="width:100%; font-size:12px;">
<thead class="thead-light">
<tr>
<th>Date</th>
<th>Invoice</th>
<th>Customer</th>
<th>Product</th>
<th>Quantity</th>
<th>UOM</th>
<th>Price</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
'''
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'''
<tr>
<td>{invoice.invoice_date.strftime('%d-%m-%Y') if invoice.invoice_date else ''}</td>
<td><b>{invoice.name or ''}</b></td>
<td>{invoice.partner_id.name or ''}</td>
<td>{line.product_id.name or ''}</td>
<td class="text-right">{quantity:.2f}</td>
<td>{line.product_uom_id.name or ''}</td>
<td class="text-right">{price:,.2f}</td>
<td class="text-right">{total:,.2f}</td>
<td>
<span class="badge badge-success">Posted</span>
</td>
</tr>
'''
total_qty += quantity
total_amount += total
# Add totals row
html_table += f'''
</tbody>
<tfoot style="background-color:#f8f9fa; font-weight:bold;">
<tr>
<td colspan="4" class="text-right"><b>Totals:</b></td>
<td class="text-right">{total_qty:.2f}</td>
<td></td>
<td></td>
<td class="text-right">{total_amount:,.2f}</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div class="alert alert-info mt-2">
<i class="fa fa-info-circle"></i>
Total Dispatch: {total_qty:.2f} units | Total Amount: {total_amount:,.2f}
</div>
'''
order.dispatch_data = html_table
else:
order.dispatch_data = '''
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
No dispatch invoices found for this order.
</div>
'''
#
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 = '''
<div class="table-responsive">
<table class="table table-bordered table-sm" style="width:100%; font-size:12px;">
<thead class="thead-light">
<tr>
<th>Production Order</th>
<th>Product</th>
<th>Planned Qty</th>
<th>Produced Qty</th>
<th>Start Date</th>
<th>End Date</th>
<th>Status</th>
<th>Work Center</th>
</tr>
</thead>
<tbody>
'''
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 = '<span class="badge badge-secondary">Draft</span>'
elif prod.state == 'confirmed':
status_badge = '<span class="badge badge-info">Confirmed</span>'
elif prod.state == 'progress':
status_badge = '<span class="badge badge-warning">In Progress</span>'
elif prod.state == 'to_close':
status_badge = '<span class="badge badge-primary">To Close</span>'
elif prod.state == 'done':
status_badge = '<span class="badge badge-success">Done</span>'
elif prod.state == 'cancel':
status_badge = '<span class="badge badge-danger">Cancelled</span>'
# Work Center
work_center = prod.workorder_ids and prod.workorder_ids[0].workcenter_id.name or 'N/A'
html_table += f'''
<tr>
<td><b>{prod.name}</b></td>
<td>{prod.product_id.name}</td>
<td class="text-right">{planned_qty:.2f}</td>
<td class="text-right">{produced_qty:.2f}</td>
<td>{start_date.strftime('%d-%m-%Y') if start_date else ''}</td>
<td>{end_date.strftime('%d-%m-%Y') if end_date else ''}</td>
<td>{status_badge}</td>
<td>{work_center}</td>
</tr>
'''
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'''
</tbody>
<tfoot style="background-color:#f8f9fa; font-weight:bold;">
<tr>
<td colspan="2" class="text-right"><b>Totals:</b></td>
<td class="text-right">{total_planned:.2f}</td>
<td class="text-right">{total_produced:.2f}</td>
<td colspan="2"></td>
<td>Efficiency: {efficiency:.1f}%</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div class="alert alert-info mt-2">
<i class="fa fa-industry"></i>
Total Planned: {total_planned:.2f} | Total Produced: {total_produced:.2f} |
Balance: {total_planned - total_produced:.2f}
</div>
'''
order.production_data = html_table
else:
order.production_data = '''
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
No production orders found for this customer order.
</div>
'''
# 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

View File

@ -0,0 +1 @@
from . import customer_orders_report

View File

@ -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 []

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_customer_orders">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page">
<h2>Customer Order: <span t-esc="doc.name"/></h2>
<div class="row mt-4">
<div class="col-6">
<h4>Order Details</h4>
<table class="table table-bordered">
<tr>
<th>Customer:</th>
<td t-esc="doc.customer_id.name"/>
</tr>
<tr>
<th>Product:</th>
<td t-esc="doc.product_id.name"/>
</tr>
<tr>
<th>Quantity:</th>
<td t-esc="doc.quantity"/>
</tr>
<tr>
<th>From Date:</th>
<td t-esc="doc.from_date"/>
</tr>
<tr>
<th>To Date:</th>
<td t-esc="doc.to_date"/>
</tr>
<tr>
<th>Status:</th>
<td t-esc="doc.state"/>
</tr>
</table>
</div>
<div class="col-6">
<h4>Progress</h4>
<div class="progress mb-3" style="height: 30px;">
<div class="progress-bar" role="progressbar"
t-att-style="'width: ' + doc.progress + '%;'"
t-att-class="'progress-bar ' + ('bg-success' if doc.progress >= 100 else 'bg-warning')">
<t t-esc="doc.progress"/>%
</div>
</div>
<table class="table table-bordered">
<tr>
<th>Ordered:</th>
<td t-esc="doc.quantity"/>
</tr>
<tr>
<th>Delivered:</th>
<td t-esc="doc.delivered_qty"/>
</tr>
<tr>
<th>Remaining:</th>
<td t-esc="doc.remaining_qty"/>
</tr>
</table>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<h4>Related Orders</h4>
<div class="row">
<div class="col-6">
<h5>Production Orders</h5>
<ul>
<t t-foreach="doc.production_orders" t-as="production">
<li t-esc="production.name"/>
</t>
</ul>
</div>
<div class="col-6">
<h5>Sale Orders</h5>
<ul>
<t t-foreach="doc.sale_lines" t-as="sale_line">
<li t-esc="sale_line.order_id.name"/>
</t>
</ul>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<h4>Summary</h4>
<table class="table table-bordered">
<tr>
<th>Total Orders in Report:</th>
<td t-esc="get_summary(docs)['total_orders']"/>
</tr>
<tr>
<th>Total Quantity:</th>
<td t-esc="get_summary(docs)['total_quantity']"/>
</tr>
<tr>
<th>Total Delivered:</th>
<td t-esc="get_summary(docs)['delivered_quantity']"/>
</tr>
<tr>
<th>Total Pending:</th>
<td t-esc="get_summary(docs)['pending_quantity']"/>
</tr>
</table>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
<report
id="action_report_customer_orders"
string="Customer Orders Report"
model="customer.order"
report_type="qweb-pdf"
name="customer_orders.report_customer_orders"
file="customer_orders.report_customer_orders"
print_report_name="'Customer Order Report - %s' % (object.name)"
/>
</odoo>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_customer_orders customer.order model_customer_order base.group_user 1 1 1 1
3 access_customer_order_line customer.order.line model_customer_order_line base.group_user 1 1 1 1

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- <record id="model_customer_orders" model="ir.model">-->
<!-- <field name="name">Customer Orders</field>-->
<!-- <field name="model">customer.orders</field>-->
<!-- <field name="info">Customer Orders Model</field>-->
<!-- <field name="access_ids" eval="[(4, ref('access_customer_orders')), (4, ref('access_customer_orders_manager'))]"/>-->
<!-- </record>-->
<record id="group_customer_orders" model="res.groups">
<field name="name">Customer Orders </field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<menuitem id="menu_customer_orders_root"
name="Customer Orders"
sequence="-2"
groups="group_customer_orders"
web_icon="customer_orders,static/description/icon.png"/>
<!-- Customer Order Form View -->
<record id="view_customer_order_form" model="ir.ui.view">
<field name="name">customer.order.form</field>
<field name="model">customer.order</field>
<field name="arch" type="xml">
<form string="Customer Order">
<header>
<button name="action_confirm" type="object" string="Confirm" class="btn-primary" invisible="state != 'draft'"/>
<button name="action_complete" type="object" string="Complete" class="btn-success" invisible="state != 'partially'"/>
<button name="action_cancel" type="object" string="Cancel" class="btn-danger" invisible="state != 'completed',"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,completed"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Order Number"/>
</h1>
</div>
<group>
<group>
<field name="customer_id" widget="res_partner_many2one" options="{'no_open': True}"/>
<field name="customer_location"/>
<field name="order_date"/>
<field name="expected_completion_date"/>
</group>
<group>
<field name="order_month"/>
<field name="order_year"/>
<field name="actual_completion_date" readonly="1"/>
<field name="completion_percentage" widget="progressbar" options="{'editable': false}"/>
</group>
</group>
<notebook>
<page string="Order Lines">
<field name="order_line_ids" mode="list,form">
<list editable="bottom">
<field name="product_id"/>
<!-- <field name="product_code"/>-->
<field name="order_qty"/>
<field name="produced_qty"/>
<field name="dispatched_qty"/>
<field name="pending_production"/>
<field name="pending_dispatch"/>
<field name="fg_available"/>
<field name="rm_shortage" widget="boolean_favorite"/>
</list>
<form>
<group>
<group>
<field name="product_id"/>
<field name="order_qty"/>
<field name="produced_qty" readonly="1"/>
<field name="dispatched_qty" readonly="1"/>
</group>
<group>
<field name="pending_production" readonly="1"/>
<field name="pending_dispatch" readonly="1"/>
<field name="fg_shortage" readonly="1"/>
<field name="rm_shortage" readonly="1"/>
</group>
</group>
<field name="rm_requirements" widget="html" readonly="1"/>
</form>
</field>
</page>
<page string="Dispatch">
<field name="dispatch_data" widget="html" readonly="1"/>
</page>
<page string="Production">
<field name="production_data" widget="html" readonly="1"/>
</page>
<page string="Notes">
<div class="alert alert-info">
<strong>Business Rules:</strong>
<ul>
<li>One order per customer per month</li>
<li>Unique product per order</li>
<li>Monthly tracking of all quantities</li>
</ul>
</div>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Customer Order list View -->
<record id="view_customer_order_list" model="ir.ui.view">
<field name="name">customer.order.list</field>
<field name="model">customer.order</field>
<field name="arch" type="xml">
<list string="Customer Orders">
<field name="name"/>
<field name="customer_id"/>
<field name="order_month"/>
<field name="order_year"/>
<field name="total_ordered_qty"/>
<field name="total_dispatched_qty"/>
<field name="balance_qty"/>
<field name="state" widget="badge" decoration-success="state=='completed'" decoration-danger="state=='cancelled'" decoration-warning="state in ['draft','confirmed']"/>
</list>
</field>
</record>
<!-- Customer Order Search View -->
<record id="view_customer_order_search" model="ir.ui.view">
<field name="name">customer.order.search</field>
<field name="model">customer.order</field>
<field name="arch" type="xml">
<search string="Customer Order">
<field name="name"/>
<field name="customer_id"/>
<field name="order_month"/>
<field name="order_year"/>
<!-- <filter string="This Month" name="this_month" domain="[('order_month','=',current_month),('order_year','=',current_year)]"/>-->
<filter string="Draft" name="draft" domain="[('state','=','draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state','=','confirmed')]"/>
<filter string="In Progress" name="in_progress" domain="[('state','=','in_progress')]"/>
<filter string="Completed" name="completed" domain="[('state','=','completed')]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="Customer" name="group_customer" context="{'group_by': 'customer_id'}"/>
<filter string="Month" name="group_month" context="{'group_by': 'order_month'}"/>
<filter string="Status" name="group_status" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<record id="view_customer_order_report_pivot" model="ir.ui.view">
<field name="name">customer.order.report.pivot</field>
<field name="model">customer.order</field>
<field name="arch" type="xml">
<pivot string="Customer Order" sample="1">
<field name="name" type="row"/>
<field name="order_month" type="col"/>
<field name="total_ordered_qty" string="Total Order Quantity" type="measure"/>
<field name="total_dispatched_qty" string="Total Dispatched Quantity" type="measure"/>
<field name="balance_qty" string="Balance Quantity" type="measure"/>
</pivot>
</field>
</record>
<record id="view_customer_order_line_report_pivot" model="ir.ui.view">
<field name="name">customer.order.line.pivot</field>
<field name="model">customer.order.line</field>
<field name="arch" type="xml">
<pivot string="Customer Orders" sample="1">
<field name="order_month" type="row"/>
<field name="customer_id" type="row"/>
<field name="product_id" type="row"/>
<field name="order_qty" string="Order Quantity" type="measure"/>
<field name="dispatched_qty" string="Dispatched Quantity" type="measure"/>
<!-- <field name="balance_qty" string="Balance Quantity" type="measure"/>-->
</pivot>
</field>
</record>
<record id="action_customer_order_line" model="ir.actions.act_window">
<field name="name">Customer Orders</field>
<field name="res_model">customer.order.line</field>
<field name="view_mode">pivot</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first customer order
</p>
</field>
</record>
<!-- Customer Order Action -->
<record id="action_customer_order" model="ir.actions.act_window">
<field name="name">Customer Orders</field>
<field name="res_model">customer.order</field>
<field name="view_mode">list,form,pivot</field>
<field name="search_view_id" ref="customer_orders.view_customer_order_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first customer order
</p>
</field>
</record>
<!-- Menu Items -->
<menuitem id="menu_customer_order" name="Customer Orders"
parent="menu_customer_orders_root" sequence="10"/>
<menuitem id="menu_customer_order_sub" name="Orders"
parent="menu_customer_order" action="action_customer_order" sequence="10"/>
<menuitem id="menu_customer_order_line" name="Orders lines"
parent="menu_customer_order" action="action_customer_order_line" sequence="10"/>
</data>
</odoo>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--&lt;!&ndash; Dashboard Action &ndash;&gt;-->
<!-- <record id="action_customer_orders_dashboard" model="ir.actions.client">-->
<!-- <field name="name">Customer Orders Dashboard</field>-->
<!-- <field name="tag">customer_orders_dashboard</field>-->
<!-- <field name="params">{}</field>-->
<!-- </record>-->
<!-- <menuitem id="menu_customer_orders_dashboard"-->
<!-- name="Dashboard"-->
<!-- parent="menu_customer_orders_root"-->
<!-- action="action_customer_orders_dashboard"-->
<!-- sequence="30"/>-->
<!-- Action -->
<record id="action_cor_dashboard" model="ir.actions.client">
<field name="name">Overview</field>
<field name="tag">CustomerOrderStatusGrid</field>
</record>
<menuitem id="menu_cor_dashboard"
name="Dashboard"
sequence="5"
parent="customer_orders.menu_customer_order"
action="action_cor_dashboard"/>
</odoo>