customer orders Module
This commit is contained in:
parent
fe335edd7c
commit
f638146824
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import reports
|
||||
from . import controllers
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import customer_orders
|
||||
from . import customer_order_line
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import customer_orders_report
|
||||
|
|
@ -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 []
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--<!– Dashboard Action –>-->
|
||||
<!-- <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>
|
||||
Loading…
Reference in New Issue