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): return self._generate_export(old_format=False) def action_export_old_popcorn(self): return self._generate_export(old_format=True) def _generate_export(self, old_format=False): 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_kwargs = {'bold': True, 'align': 'center', 'valign': 'vcenter'} if not old_format: header_kwargs['border'] = 1 header_format = workbook.add_format(header_kwargs) 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'}) if old_format: headers = [ "No", "Date", "Outlet", "Table/Customer", "Invoice", "Category", "SKU", "Product", "Quantity", "Price Type", "Price", "Price Cut", "Subtotal", "Discount", "Tax", "Service", "Rounding", "Charge", "Paid", "Pax", "Return", "Refund", "Payment", "Note", "Dinein", "User" ] else: 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 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:Z1' if old_format else 'A1:AF1', 'MIE MAPAN', title_format) sheet.merge_range('A2:Z2' if old_format else '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: if 'refund_orders_count' in order._fields and order.refund_orders_count > 0: continue 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 = str(order.table_id.table_number) if 'table_id' in order._fields and order.table_id else '' customer = order.partner_id.name or '' floating_order_name = order.floating_order_name if 'floating_order_name' in order._fields and order.floating_order_name else '' table_customer = table or floating_order_name or customer invoice = order.pos_reference or order.name subtotal = sum(l.price_subtotal for l in order.lines) discount_order = sum(abs(l.price_subtotal) for l in order.lines if l.price_unit < 0) tax = order.amount_tax paid = sum(p.amount for p in order.payment_ids if p.amount > 0) if paid == 0: continue rounding = order.amount_paid - order.amount_total charge = order.amount_paid 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 - order.amount_total if paid > order.amount_total else 0) payment_methods = ', '.join(order.payment_ids.mapped('payment_method_id.name')) note = order.note if 'note' in order._fields else '' if 'table_id' in order._fields and order.table_id: dinein = "dinein" else: dinein = "takeaway" user = order.user_id.name or '' is_first_line = True for line in order.lines: if old_format and line.price_unit < 0: continue if old_format and not is_first_line: sheet.write_string(row_num, 0, "") sheet.write_string(row_num, 1, "") sheet.write_string(row_num, 2, "") sheet.write_string(row_num, 3, "") sheet.write_string(row_num, 4, "") else: 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) else: sheet.write_string(row_num, 1, "") 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_bc_item_id if 'x_studio_bc_item_id' in line.product_id._fields and line.product_id.x_studio_bc_item_id 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 if old_format: sheet.write(row_num, 16, rounding, number_format) # Rounding sheet.write(row_num, 17, charge, number_format) sheet.write(row_num, 18, paid, number_format) sheet.write(row_num, 19, pax) sheet.write(row_num, 20, return_amt, number_format) sheet.write(row_num, 21, 0, number_format) # Refund sheet.write(row_num, 22, payment_methods) sheet.write(row_num, 23, note) sheet.write(row_num, 24, dinein) sheet.write(row_num, 25, user) else: 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, rounding, 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) company = self.env.company company_ident = (company.company_registry or company.name or '').upper().replace(' ', '') # Max length logic # For old_format: len("POS__YYMMDD_to_YYMMDD_OLD.xlsx") = 30 chars -> leaves 10 chars for company_ident # For new_format: len("POS__YYMMDD_to_YYMMDD.xlsx") = 26 chars -> leaves 14 chars for company_ident company_len = 10 if old_format else 14 company_ident = company_ident[:company_len] start_str = self.start_date.strftime('%y%m%d') end_str = self.end_date.strftime('%y%m%d') if old_format: file_name = f"POS_{company_ident}_{start_str}_to_{end_str}_OLD.xlsx" else: file_name = f"POS_{company_ident}_{start_str}_to_{end_str}.xlsx" # Save as an ir.attachment and return action to download attachment = self.env['ir.attachment'].create({ 'name': file_name, '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', }