158 lines
5.9 KiB
Python
Executable File
158 lines
5.9 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
|
|
from odoo import models, fields, api
|
|
from odoo.exceptions import ValidationError
|
|
from datetime import datetime
|
|
import base64
|
|
from io import BytesIO
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Font, Alignment, Border, Side
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
class StockMovementReportWizard(models.TransientModel):
|
|
_name = 'stock_movement_report_wizard'
|
|
_description = 'Stock Movement Report Wizard'
|
|
|
|
date_from = fields.Date(string='From Date', required=True, default=lambda self: datetime.now().replace(day=1))
|
|
date_to = fields.Date(string='To Date', required=True, default=lambda self: datetime.now())
|
|
excel_file = fields.Binary(string='Excel Report', readonly=True)
|
|
file_name = fields.Char(string='File Name', readonly=True)
|
|
|
|
def generate_excel_report(self):
|
|
"""Generate and download the Excel report."""
|
|
if self.date_from > self.date_to:
|
|
raise ValidationError("From Date must be before To Date.")
|
|
|
|
# Prepare data and generate Excel file
|
|
data = self._prepare_report_data()
|
|
excel_file = self._generate_excel(data)
|
|
|
|
# Save the file and ensure the record is persisted
|
|
self.write({
|
|
'excel_file': excel_file,
|
|
'file_name': f"Stock_Movement_Report_{self.date_from}_to_{self.date_to}.xlsx"
|
|
})
|
|
|
|
# Commit the transaction to ensure the record is saved
|
|
self.env.cr.commit()
|
|
|
|
# Verify the file was saved
|
|
if not self.excel_file:
|
|
raise ValidationError("Failed to generate or save the Excel file.")
|
|
|
|
# Return URL-based download action
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': f'/web/content?model={self._name}&id={self.id}&field=excel_file&filename_field=file_name&download=true',
|
|
'target': 'self',
|
|
}
|
|
|
|
def _prepare_report_data(self):
|
|
"""Prepare data for full stock movement summary report."""
|
|
domain = [('date', '>=', self.date_from), ('date', '<=', self.date_to)]
|
|
transactions = self.env['sos_material_transaction_history'].search(domain)
|
|
|
|
# Build material data with in/out qty and price
|
|
material_summary = {}
|
|
for trans in transactions:
|
|
mat = trans.ref_id
|
|
if mat.id not in material_summary:
|
|
material_summary[mat.id] = {
|
|
'part_no': mat.part_no,
|
|
'name': mat.name or '',
|
|
'uom': mat.uom,
|
|
'price': round(mat.unit_price or 0.0, 2),
|
|
'opening_bal_qty':mat.opening_bal_qty,
|
|
'in_qty': 0.0,
|
|
'out_qty': 0.0,
|
|
}
|
|
if trans.action == 'in':
|
|
material_summary[mat.id]['in_qty'] += trans.quantity
|
|
elif trans.action == 'out':
|
|
material_summary[mat.id]['out_qty'] += trans.quantity
|
|
|
|
# Include materials with no transactions as well
|
|
all_materials = self.env['sos_material'].search([])
|
|
for mat in all_materials:
|
|
if mat.id not in material_summary:
|
|
material_summary[mat.id] = {
|
|
'part_no': mat.part_no,
|
|
'name': mat.name or '',
|
|
'uom': mat.uom,
|
|
'price': round(mat.unit_price or 0.0, 2),
|
|
'opening_bal_qty':mat.opening_bal_qty,
|
|
'in_qty': 0.0,
|
|
'out_qty': 0.0,
|
|
}
|
|
|
|
return {
|
|
'date_from': self.date_from,
|
|
'date_to': self.date_to,
|
|
'summary': list(material_summary.values())
|
|
}
|
|
|
|
|
|
def _generate_excel(self, data):
|
|
"""Generate Excel file showing all material movements within date range."""
|
|
|
|
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.title = "All Material Movement"
|
|
|
|
# Styles
|
|
header_font = Font(bold=True)
|
|
header_align = Alignment(horizontal='center', vertical='center')
|
|
border = Border(
|
|
left=Side(style='thin'),
|
|
right=Side(style='thin'),
|
|
top=Side(style='thin'),
|
|
bottom=Side(style='thin'),
|
|
)
|
|
|
|
# Title
|
|
ws['A1'] = f"Material Movement Report ({data['date_from']} to {data['date_to']})"
|
|
ws.merge_cells('A1:F1')
|
|
ws['A1'].font = Font(bold=True, size=14)
|
|
ws['A1'].alignment = header_align
|
|
|
|
# Header Row
|
|
row = 3
|
|
headers = ["Part No", "Name", "UOM", "Price","Opening Balance(Sep 2-2024)", "In Qty", "Out Qty"]
|
|
for col, header in enumerate(headers, 1):
|
|
cell = ws.cell(row=row, column=col)
|
|
cell.value = header
|
|
cell.font = header_font
|
|
cell.alignment = header_align
|
|
cell.border = border
|
|
row += 1
|
|
|
|
# Data Rows
|
|
for stock in data['summary']:
|
|
ws.cell(row=row, column=1).value = stock['part_no']
|
|
ws.cell(row=row, column=2).value = stock['name']
|
|
ws.cell(row=row, column=3).value = stock['uom']
|
|
ws.cell(row=row, column=4).value = round(stock['price'], 2)
|
|
ws.cell(row=row, column=5).value = stock['opening_bal_qty']
|
|
ws.cell(row=row, column=6).value = stock['in_qty']
|
|
ws.cell(row=row, column=7).value = stock['out_qty']
|
|
for col in range(1, 8):
|
|
ws.cell(row=row, column=col).border = border
|
|
row += 1
|
|
|
|
# Adjust column widths
|
|
for col in range(1, 8):
|
|
max_length = 0
|
|
col_letter = get_column_letter(col)
|
|
for r in ws.iter_rows(min_row=1, max_row=ws.max_row, min_col=col, max_col=col):
|
|
for cell in r:
|
|
if cell.value:
|
|
max_length = max(max_length, len(str(cell.value)))
|
|
ws.column_dimensions[col_letter].width = max_length + 2
|
|
|
|
# Save to BytesIO and return as base64
|
|
output = BytesIO()
|
|
wb.save(output)
|
|
output.seek(0)
|
|
return base64.b64encode(output.read())
|