import base64 import io from datetime import datetime import pytz from odoo import api, fields, models, _ from odoo.exceptions import UserError import odoo.tools try: import xlsxwriter except ImportError: xlsxwriter = None class PosExportBcWizard(models.TransientModel): _name = 'pos.export.bc.wizard' _description = 'POS Export BC Format Wizard' start_date = fields.Date(string="Start Date", required=True, default=fields.Date.context_today) end_date = fields.Date(string="End Date", required=True, default=fields.Date.context_today) def action_export_bc(self): self.ensure_one() if not xlsxwriter: raise UserError(_("The Python library 'xlsxwriter' is required. Please install it.")) if self.start_date > self.end_date: raise UserError(_("Start Date must be earlier or equal to End Date.")) output = io.BytesIO() workbook = xlsxwriter.Workbook(output, {'in_memory': True}) # Define formats header_format = workbook.add_format({ 'bold': True, 'align': 'center', 'valign': 'vcenter', 'border': 1 }) title_format = workbook.add_format({ 'bold': True, 'align': 'center', 'valign': 'vcenter', 'font_color': 'red', 'size': 14 }) subtitle_format = workbook.add_format({ 'bold': True, 'align': 'center', 'valign': 'vcenter', 'font_color': 'black', 'size': 12 }) date_format = workbook.add_format({'num_format': 'dd-mm-yyyy'}) date_time_format = workbook.add_format({'num_format': 'dd-mm-yyyy hh:mm:ss'}) number_format = workbook.add_format({'num_format': '#,##0.00'}) headers = [ "No", "Date", "Outlet", "Table/Customer", "Invoice", "Category", "SKU", "Product", "Quantity", "Price Type", "Price", "Price Cut", "Subtotal", "Discount", "Tax", "Service", "Takeaway Charge", "Packaging Fee", "Rounding", "Charge", "Paid", "Pax", "Paid At", "Return", "Refund", "Payment", "Note", "Dinein", "User", "Promo", "Order from", "Nama Penerima", "Alamat Penerima", "Link Maps" ] # Datetime timezone conversion # We need these in UTC for the domain search user_tz_str = self.env.user.tz or 'UTC' user_tz = pytz.timezone(user_tz_str) start_datetime = datetime.combine(self.start_date, datetime.min.time()) end_datetime = datetime.combine(self.end_date, datetime.max.time()) start_utc = user_tz.localize(start_datetime).astimezone(pytz.UTC).replace(tzinfo=None) end_utc = user_tz.localize(end_datetime).astimezone(pytz.UTC).replace(tzinfo=None) def write_sheet(sheet_name, domain): sheet = workbook.add_worksheet(sheet_name) # Title sheet.merge_range('A1:AF1', 'MIE MAPAN', title_format) sheet.merge_range('A2:AF2', 'INVOICES', subtitle_format) # Period start_dt_str = start_datetime.strftime('%d-%m-%Y 00:00') end_dt_str = end_datetime.strftime('%d-%m-%Y 23:59') sheet.write('A3', 'Period') sheet.write('B3', ':') sheet.write('C3', f"{start_dt_str} - {end_dt_str}") # Headers for col_num, header in enumerate(headers): sheet.write(4, col_num, header, header_format) row_num = 5 orders = self.env['pos.order'].search(domain, order='date_order asc') order_no = 1 for order in orders: local_date = order.date_order.replace(tzinfo=pytz.UTC).astimezone(user_tz) if order.date_order else False outlet = order.config_id.name or '' table = order.table_id.display_name if 'table_id' in order._fields and order.table_id else '' customer = order.partner_id.name or '' table_customer = table or customer invoice = order.pos_reference or order.name subtotal = sum(l.price_subtotal for l in order.lines) discount_order = 0.0 # Standard Odoo doesn't have an order level discount easily isolated from line discounts tax = order.amount_tax charge = order.amount_total # In Odoo, payment amount can be negative for change. # To get the total amount tendered before change, we sum only the positive payments. # The change/return is typically stored in order.amount_return paid = sum(p.amount for p in order.payment_ids if p.amount > 0) pax = order.customer_count if 'customer_count' in order._fields else 1 return_amt = order.amount_return if 'amount_return' in order._fields else (paid - charge if paid > charge else 0) payment_methods = ', '.join(order.payment_ids.mapped('payment_method_id.name')) note = order.note if 'note' in order._fields else '' # dinein = 'dinein' if table else 'takeaway' preset = order.preset_id.name if "dine" in preset.lower(): dinein = "dinein" else: dinein = "takeaway" user = order.user_id.name or '' is_first_line = True for line in order.lines: sheet.write(row_num, 0, order_no) if local_date: date_str = local_date.strftime('%d-%m-%Y %H:%M:%S') sheet.write_string(row_num, 1, date_str) sheet.write(row_num, 2, outlet) sheet.write(row_num, 3, table_customer) sheet.write(row_num, 4, invoice) category = line.product_id.pos_categ_ids[0].name if line.product_id.pos_categ_ids else '' sku = line.product_id.x_studio_popcorn_sku if 'x_studio_popcorn_sku' in line.product_id._fields and line.product_id.x_studio_popcorn_sku else '' product_name = line.product_id.name or '' qty = line.qty price_type = order.pricelist_id.name or 'DEFAULT' price = line.price_unit price_cut = (line.price_unit * line.discount / 100) if line.discount else 0.0 sheet.write(row_num, 5, category) sheet.write(row_num, 6, sku) sheet.write(row_num, 7, product_name) sheet.write(row_num, 8, qty) sheet.write(row_num, 9, price_type) sheet.write(row_num, 10, price, number_format) sheet.write(row_num, 11, price_cut, number_format) if is_first_line: sheet.write(row_num, 12, subtotal, number_format) sheet.write(row_num, 13, discount_order, number_format) sheet.write(row_num, 14, tax, number_format) sheet.write(row_num, 15, 0, number_format) # Service sheet.write(row_num, 16, 0, number_format) # Takeaway Charge sheet.write(row_num, 17, 0, number_format) # Packaging Fee sheet.write(row_num, 18, 0, number_format) # Rounding sheet.write(row_num, 19, charge, number_format) sheet.write(row_num, 20, paid, number_format) sheet.write(row_num, 21, pax) if local_date: date_str = local_date.strftime('%d-%m-%Y %H:%M:%S') sheet.write_string(row_num, 22, date_str) sheet.write(row_num, 23, return_amt, number_format) sheet.write(row_num, 24, 0, number_format) # Refund sheet.write(row_num, 25, payment_methods) sheet.write(row_num, 26, note) sheet.write(row_num, 27, dinein) sheet.write(row_num, 28, user) sheet.write(row_num, 29, "") # Promo sheet.write(row_num, 30, "cashier") # Order from sheet.write(row_num, 31, "") sheet.write(row_num, 32, "") sheet.write(row_num, 33, "") is_first_line = False row_num += 1 order_no += 1 domain_base = [('date_order', '>=', start_utc), ('date_order', '<=', end_utc)] write_sheet('Invoice', domain_base + [('amount_total', '>=', 0)]) write_sheet('Refund', domain_base + [('amount_total', '<', 0)]) workbook.close() output.seek(0) # Save as an ir.attachment and return action to download attachment = self.env['ir.attachment'].create({ 'name': f"POS_BC_{self.start_date}_to_{self.end_date}.xlsx", 'type': 'binary', 'datas': base64.b64encode(output.read()), 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) return { 'type': 'ir.actions.act_url', 'url': f'/web/content/{attachment.id}?download=true', 'target': 'self', }