Initial commit
This commit is contained in:
617
fusion_claims/wizard/adp_export_wizard.py
Normal file
617
fusion_claims/wizard/adp_export_wizard.py
Normal file
@@ -0,0 +1,617 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2025 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Claim Assistant product family.
|
||||
|
||||
import base64
|
||||
from datetime import datetime, date
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionCentralExportWizard(models.TransientModel):
|
||||
_name = 'fusion_claims.export.wizard'
|
||||
_description = 'Fusion Central ADP Export Wizard'
|
||||
|
||||
invoice_ids = fields.Many2many(
|
||||
'account.move',
|
||||
string='Invoices',
|
||||
domain=[('move_type', 'in', ['out_invoice', 'out_refund'])],
|
||||
)
|
||||
vendor_code = fields.Char(
|
||||
string='Vendor Code',
|
||||
required=True,
|
||||
)
|
||||
export_date = fields.Date(
|
||||
string='Export Date',
|
||||
default=fields.Date.today,
|
||||
required=True,
|
||||
)
|
||||
|
||||
# Export result
|
||||
export_file = fields.Binary(string='Export File', readonly=True)
|
||||
export_filename = fields.Char(string='Filename', readonly=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('done', 'Done'),
|
||||
], default='draft')
|
||||
export_summary = fields.Text(string='Export Summary', readonly=True)
|
||||
saved_to_documents = fields.Boolean(string='Saved to Documents', readonly=True)
|
||||
warnings = fields.Text(string='Warnings', readonly=True)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
|
||||
# Get vendor code from settings
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
res['vendor_code'] = ICP.get_param('fusion_claims.vendor_code', '')
|
||||
|
||||
# Get invoices from context
|
||||
if self._context.get('active_ids'):
|
||||
invoices = self.env['account.move'].browse(self._context['active_ids'])
|
||||
# Filter to only customer invoices/refunds
|
||||
invoices = invoices.filtered(lambda m: m.move_type in ['out_invoice', 'out_refund'])
|
||||
res['invoice_ids'] = [(6, 0, invoices.ids)]
|
||||
|
||||
return res
|
||||
|
||||
def _get_field_value(self, record, field_name, default=''):
|
||||
"""Get field value safely."""
|
||||
return getattr(record, field_name, default) or default
|
||||
|
||||
def _format_date(self, date_val):
|
||||
"""Format date as ddmmyyyy."""
|
||||
if not date_val:
|
||||
return ''
|
||||
if isinstance(date_val, str):
|
||||
try:
|
||||
date_val = datetime.strptime(date_val, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return ''
|
||||
return date_val.strftime('%d%m%Y')
|
||||
|
||||
def _should_skip_line(self, line):
|
||||
"""Check if line should be skipped based on device code."""
|
||||
code = line._get_adp_device_code().upper() if hasattr(line, '_get_adp_device_code') else ''
|
||||
skip_codes = ['FUNDING', 'NON-FUNDED', 'N/A', 'NA', 'NON-ADP', 'LABOUR', 'DELIVERY', '']
|
||||
return code in skip_codes or not code
|
||||
|
||||
def _validate_dates(self, invoice):
|
||||
"""Validate dates and return warnings."""
|
||||
warnings = []
|
||||
today = date.today()
|
||||
|
||||
invoice_date = invoice.invoice_date
|
||||
delivery_date = invoice.x_fc_adp_delivery_date
|
||||
|
||||
# Check for future dates
|
||||
if invoice_date and invoice_date > today:
|
||||
warnings.append(f"Invoice {invoice.name}: Invoice date is in the future")
|
||||
|
||||
if delivery_date and delivery_date > today:
|
||||
warnings.append(f"Invoice {invoice.name}: Delivery date is in the future")
|
||||
|
||||
# Check delivery date vs invoice date
|
||||
if delivery_date and invoice_date and delivery_date > invoice_date:
|
||||
warnings.append(f"Invoice {invoice.name}: Delivery date is after invoice date")
|
||||
|
||||
return warnings
|
||||
|
||||
def _verify_portions(self, adp_device_price, client_type, deduction_type, deduction_value,
|
||||
stored_adp_portion, stored_client_portion, quantity=1, tolerance=0.01):
|
||||
"""Verify stored portions against calculated portions.
|
||||
|
||||
Recalculates portions using the ADP database price and compares with stored values.
|
||||
This ensures the invoice calculations match the ADP database.
|
||||
|
||||
IMPORTANT: Invoice lines store TOTAL portions (qty × unit portion).
|
||||
This method calculates unit portions and multiplies by quantity for comparison.
|
||||
|
||||
Args:
|
||||
adp_device_price: ADP approved price from database (per unit)
|
||||
client_type: Client type (REG, ODS, OWP, ACS, LTC, SEN, CCA)
|
||||
deduction_type: 'pct', 'amt', 'none', or None
|
||||
deduction_value: Deduction value (per unit for AMT type)
|
||||
stored_adp_portion: Pre-calculated TOTAL ADP portion from invoice line
|
||||
stored_client_portion: Pre-calculated TOTAL client portion from invoice line
|
||||
quantity: Line quantity (used to calculate expected totals)
|
||||
tolerance: Acceptable difference for floating point comparison (default $0.01)
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, expected_adp_total, expected_client_total, error_message)
|
||||
"""
|
||||
# Determine base percentage by client type
|
||||
if client_type == 'REG':
|
||||
base_adp_pct = 0.75
|
||||
else:
|
||||
# ODS, OWP, ACS, LTC, SEN, CCA: 100% ADP, 0% Client
|
||||
base_adp_pct = 1.0
|
||||
|
||||
# Calculate expected PER-UNIT portions (same logic as invoice)
|
||||
if deduction_type == 'pct' and deduction_value:
|
||||
# PCT: ADP covers (deduction_value)% of their normal portion
|
||||
effective_pct = base_adp_pct * (deduction_value / 100)
|
||||
unit_adp = adp_device_price * effective_pct
|
||||
elif deduction_type == 'amt' and deduction_value:
|
||||
# AMT: Subtract fixed amount from base ADP portion (per unit)
|
||||
base_adp = adp_device_price * base_adp_pct
|
||||
unit_adp = max(0, base_adp - deduction_value)
|
||||
else:
|
||||
# No deduction
|
||||
unit_adp = adp_device_price * base_adp_pct
|
||||
|
||||
unit_client = adp_device_price - unit_adp
|
||||
|
||||
# Calculate expected TOTALS (unit × quantity)
|
||||
expected_adp_total = unit_adp * quantity
|
||||
expected_client_total = unit_client * quantity
|
||||
|
||||
# Compare with stored values (allow small tolerance for rounding)
|
||||
# Tolerance scales with quantity to account for accumulated rounding
|
||||
scaled_tolerance = tolerance * max(1, quantity)
|
||||
adp_diff = abs(stored_adp_portion - expected_adp_total)
|
||||
client_diff = abs(stored_client_portion - expected_client_total)
|
||||
|
||||
if adp_diff > scaled_tolerance or client_diff > scaled_tolerance:
|
||||
error_msg = (
|
||||
f"Calculation mismatch!\n"
|
||||
f" ADP Device Price: ${adp_device_price:.2f} × {quantity} = ${adp_device_price * quantity:.2f}\n"
|
||||
f" Client Type: {client_type} ({int(base_adp_pct*100)}% ADP)\n"
|
||||
f" Deduction: {deduction_type or 'None'}"
|
||||
f"{f' = {deduction_value}' if deduction_value else ''}\n"
|
||||
f" Per Unit: ADP=${unit_adp:.2f}, Client=${unit_client:.2f}\n"
|
||||
f" Expected Total (×{quantity}): ADP=${expected_adp_total:.2f}, Client=${expected_client_total:.2f}\n"
|
||||
f" Invoice has: ADP=${stored_adp_portion:.2f}, Client=${stored_client_portion:.2f}\n"
|
||||
f" Difference: ADP=${adp_diff:.2f}, Client=${client_diff:.2f}"
|
||||
)
|
||||
return False, expected_adp_total, expected_client_total, error_msg
|
||||
|
||||
return True, expected_adp_total, expected_client_total, None
|
||||
|
||||
def _generate_claim_lines(self, invoice):
|
||||
"""Generate claim lines for an invoice.
|
||||
|
||||
Uses PRE-CALCULATED values from invoice lines (x_fc_adp_portion, x_fc_client_portion)
|
||||
but VERIFIES them against the ADP database before export.
|
||||
|
||||
This ensures:
|
||||
1. Single source of truth - values from invoice
|
||||
2. Verification - calculations match ADP database
|
||||
3. Deductions included in verification
|
||||
"""
|
||||
lines = []
|
||||
verification_errors = []
|
||||
ADPDevice = self.env['fusion.adp.device.code'].sudo()
|
||||
|
||||
client_type = invoice._get_client_type() or 'REG'
|
||||
invoice_date = invoice.invoice_date or invoice.date
|
||||
delivery_date = invoice.x_fc_adp_delivery_date
|
||||
claim_number = invoice.x_fc_claim_number or ''
|
||||
client_ref_2 = invoice.x_fc_client_ref_2 or ''
|
||||
|
||||
for line in invoice.invoice_line_ids:
|
||||
# Skip non-product lines
|
||||
if not line.product_id or line.display_type in ('line_section', 'line_note'):
|
||||
continue
|
||||
|
||||
# Skip lines with excluded device codes
|
||||
if self._should_skip_line(line):
|
||||
continue
|
||||
|
||||
# Get device code
|
||||
device_code = line._get_adp_device_code()
|
||||
if not device_code:
|
||||
continue
|
||||
|
||||
# Get ADP approved device price from database for verification
|
||||
adp_device = ADPDevice.search([
|
||||
('device_code', '=', device_code),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not adp_device:
|
||||
_logger.warning(f"ADP device code {device_code} not found in database, skipping line")
|
||||
continue
|
||||
|
||||
adp_device_price = adp_device.adp_price or 0
|
||||
if not adp_device_price:
|
||||
_logger.warning(f"ADP device {device_code} has no price, skipping line")
|
||||
continue
|
||||
|
||||
# Get quantity first (needed for verification)
|
||||
qty = int(line.quantity) if line.quantity else 1
|
||||
|
||||
# Get PRE-CALCULATED TOTAL portions from invoice line
|
||||
# These are TOTALS (unit × qty), calculated when the invoice was created
|
||||
stored_adp_portion = line.x_fc_adp_portion or 0
|
||||
stored_client_portion = line.x_fc_client_portion or 0
|
||||
|
||||
# Get deduction info for verification
|
||||
deduction_type = line.x_fc_deduction_type or 'none'
|
||||
deduction_value = line.x_fc_deduction_value or 0
|
||||
|
||||
# VERIFY: Recalculate and compare with stored values
|
||||
# Pass quantity so verification compares totals correctly
|
||||
is_valid, expected_adp, expected_client, error_msg = self._verify_portions(
|
||||
adp_device_price=adp_device_price,
|
||||
client_type=client_type,
|
||||
deduction_type=deduction_type if deduction_type != 'none' else None,
|
||||
deduction_value=deduction_value,
|
||||
stored_adp_portion=stored_adp_portion,
|
||||
stored_client_portion=stored_client_portion,
|
||||
quantity=qty,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
verification_errors.append(
|
||||
f"Invoice {invoice.name}, Line: {line.product_id.name} ({device_code})\n{error_msg}"
|
||||
)
|
||||
continue # Skip lines with mismatched calculations
|
||||
|
||||
serial_number = line._get_serial_number()
|
||||
|
||||
# Calculate PER-UNIT portions for export (each export line is qty=1)
|
||||
unit_adp_portion = stored_adp_portion / qty if qty > 0 else stored_adp_portion
|
||||
unit_client_portion = stored_client_portion / qty if qty > 0 else stored_client_portion
|
||||
|
||||
# Export one line per unit (ADP expects qty=1 per line)
|
||||
for i in range(qty):
|
||||
lines.append({
|
||||
'vendor_code': self.vendor_code,
|
||||
'claim_number': claim_number,
|
||||
'client_ref_2': client_ref_2,
|
||||
'invoice_number': invoice.name,
|
||||
'invoice_date': self._format_date(invoice_date),
|
||||
'delivery_date': self._format_date(delivery_date),
|
||||
'device_code': device_code,
|
||||
'serial_number': serial_number,
|
||||
'qty': '1',
|
||||
'device_price': f"{adp_device_price:.2f}",
|
||||
'adp_portion': f"{unit_adp_portion:.2f}",
|
||||
'client_portion': f"{unit_client_portion:.2f}",
|
||||
'client_type': client_type,
|
||||
})
|
||||
|
||||
# If there were verification errors, raise them
|
||||
if verification_errors:
|
||||
raise UserError(_(
|
||||
"ADP Export Verification Failed!\n\n"
|
||||
"The following invoice lines have calculation mismatches between "
|
||||
"the invoice and the ADP device database:\n\n%s\n\n"
|
||||
"Please verify the invoice calculations and ADP device prices."
|
||||
) % '\n\n'.join(verification_errors))
|
||||
|
||||
return lines
|
||||
|
||||
def _generate_export_content(self):
|
||||
"""Generate the full export content."""
|
||||
all_lines = []
|
||||
all_warnings = []
|
||||
|
||||
for invoice in self.invoice_ids:
|
||||
if not invoice._is_adp_invoice():
|
||||
continue
|
||||
|
||||
# Validate dates
|
||||
all_warnings.extend(self._validate_dates(invoice))
|
||||
|
||||
# Generate lines
|
||||
all_lines.extend(self._generate_claim_lines(invoice))
|
||||
|
||||
if not all_lines:
|
||||
raise UserError(_("No valid lines to export. Make sure invoices are ADP type and have valid products."))
|
||||
|
||||
# Build CSV content (no header)
|
||||
# ADP Format: vendor_code,claim_number,client_ref_2,invoice_number,invoice_date,delivery_date,
|
||||
# ,,device_code,serial_number,,qty,device_price,adp_portion,client_portion,client_type
|
||||
# Note: device_price is the ADP approved price from fusion.adp.device.code database
|
||||
content_lines = []
|
||||
for line in all_lines:
|
||||
row = ','.join([
|
||||
line['vendor_code'],
|
||||
line['claim_number'],
|
||||
line['client_ref_2'],
|
||||
line['invoice_number'],
|
||||
line['invoice_date'],
|
||||
line['delivery_date'],
|
||||
'', # Reserved field 1 (empty)
|
||||
'', # Reserved field 2 (empty)
|
||||
line['device_code'],
|
||||
line['serial_number'],
|
||||
'', # Reserved field 3 (empty)
|
||||
line['qty'],
|
||||
line['device_price'],
|
||||
line['adp_portion'],
|
||||
line['client_portion'],
|
||||
line['client_type'],
|
||||
])
|
||||
content_lines.append(row)
|
||||
|
||||
return '\n'.join(content_lines), len(all_lines), all_warnings
|
||||
|
||||
def _get_export_filename(self):
|
||||
"""Generate filename for ADP export.
|
||||
|
||||
ADP requires a specific filename format: VENDORCODE_YYYY-MM-DD.txt
|
||||
We do NOT add submission numbers because ADP won't accept renamed files.
|
||||
User must manually rename if submitting multiple times on same day.
|
||||
"""
|
||||
return f"{self.vendor_code}_{self.export_date.strftime('%Y-%m-%d')}.txt"
|
||||
|
||||
def _check_existing_file(self, filename):
|
||||
"""Check if a file with the same name already exists in Documents.
|
||||
|
||||
Returns:
|
||||
tuple: (exists: bool, existing_files: list of names)
|
||||
"""
|
||||
existing_files = []
|
||||
|
||||
if 'documents.document' not in self.env:
|
||||
return False, existing_files
|
||||
|
||||
try:
|
||||
# Get the folder where we save files
|
||||
folder = self._get_or_create_documents_folder()
|
||||
if not folder:
|
||||
return False, existing_files
|
||||
|
||||
# Search for files with the same name in our folder
|
||||
existing = self.env['documents.document'].search([
|
||||
('name', '=', filename),
|
||||
('type', '=', 'binary'),
|
||||
('folder_id', '=', folder.id),
|
||||
])
|
||||
|
||||
if existing:
|
||||
existing_files = [f"{doc.name} (created: {doc.create_date.strftime('%Y-%m-%d %H:%M')})"
|
||||
for doc in existing]
|
||||
return True, existing_files
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning("Error checking existing files: %s", str(e))
|
||||
|
||||
return False, existing_files
|
||||
|
||||
def action_export(self):
|
||||
"""Perform the export.
|
||||
|
||||
Flow:
|
||||
1. Validate inputs
|
||||
2. Generate content (includes verification - may raise UserError)
|
||||
3. Check for existing files (warn but don't block)
|
||||
4. Show download window
|
||||
5. Save to Documents ONLY after successful generation
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.invoice_ids:
|
||||
raise UserError(_("Please select at least one invoice to export."))
|
||||
|
||||
if not self.vendor_code:
|
||||
raise UserError(_("Please enter a vendor code."))
|
||||
|
||||
# Generate filename first
|
||||
filename = self._get_export_filename()
|
||||
|
||||
# Check for existing file with same name BEFORE generating content
|
||||
file_exists, existing_files = self._check_existing_file(filename)
|
||||
|
||||
# Generate content - this includes all validation and verification
|
||||
# If verification fails, UserError is raised and we don't save anything
|
||||
content, line_count, warnings = self._generate_export_content()
|
||||
|
||||
# If we got here, content generation was successful (no errors)
|
||||
file_data = base64.b64encode(content.encode('utf-8'))
|
||||
|
||||
# Add warning if file already exists
|
||||
if file_exists:
|
||||
warnings.append(
|
||||
f"WARNING: A file with the name '{filename}' already exists in Documents.\n"
|
||||
f"Existing files: {', '.join(existing_files)}\n"
|
||||
f"ADP does not accept renamed files. You will need to manually rename "
|
||||
f"before submitting if this is a resubmission."
|
||||
)
|
||||
|
||||
# Build warnings text BEFORE saving (so user sees the warning)
|
||||
warnings_text = '\n'.join(warnings) if warnings else ''
|
||||
|
||||
# Now save to Documents (only after successful generation)
|
||||
saved = self._save_to_documents(filename, content)
|
||||
|
||||
# Update invoices as exported
|
||||
for invoice in self.invoice_ids:
|
||||
invoice.write({
|
||||
'adp_exported': True,
|
||||
'adp_export_date': fields.Datetime.now(),
|
||||
'adp_export_count': invoice.adp_export_count + 1,
|
||||
})
|
||||
|
||||
# Build summary
|
||||
summary = _("Exported %d lines from %d invoices.") % (line_count, len(self.invoice_ids))
|
||||
if saved:
|
||||
summary += "\n" + _("File saved to Documents: ADP Billing Files/%s/%s/") % (
|
||||
date.today().year, date.today().strftime('%B')
|
||||
)
|
||||
|
||||
# Update wizard with results
|
||||
self.write({
|
||||
'export_file': file_data,
|
||||
'export_filename': filename,
|
||||
'state': 'done',
|
||||
'export_summary': summary,
|
||||
'saved_to_documents': saved,
|
||||
'warnings': warnings_text,
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def _save_to_documents(self, filename, content):
|
||||
"""Save export file to Documents app if available."""
|
||||
if 'documents.document' not in self.env:
|
||||
return False
|
||||
|
||||
try:
|
||||
Documents = self.env['documents.document']
|
||||
|
||||
# Get or create folder structure (in Company workspace)
|
||||
folder = self._get_or_create_documents_folder()
|
||||
if not folder:
|
||||
return False
|
||||
|
||||
# Create document in the Company workspace folder
|
||||
# Don't set company_id or owner_id - inherits from folder (Company workspace)
|
||||
Documents.sudo().create({
|
||||
'name': filename,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(content.encode('utf-8')),
|
||||
'folder_id': folder.id,
|
||||
'access_internal': 'edit', # Allow internal users to access
|
||||
})
|
||||
|
||||
_logger.info("Saved ADP export to Documents: %s", filename)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning("Could not save to Documents: %s", str(e))
|
||||
return False
|
||||
|
||||
def _get_or_create_documents_folder(self):
|
||||
"""Get or create the ADP Billing Files folder structure."""
|
||||
if 'documents.document' not in self.env:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Odoo 18/19 stores folders as documents.document with type='folder'
|
||||
# The documents.folder model doesn't exist in newer versions
|
||||
return self._get_or_create_folder_v18()
|
||||
except Exception as e:
|
||||
_logger.warning("Could not create folder: %s", str(e))
|
||||
return False
|
||||
|
||||
def _get_or_create_folder_v18(self):
|
||||
"""Get or create folders for Odoo 18+.
|
||||
|
||||
In Odoo 18/19, folders are stored as documents.document records with type='folder'.
|
||||
To make folders appear in Company workspace (not My Drive), we need:
|
||||
- company_id = False (not set)
|
||||
- owner_id = False (not set)
|
||||
- access_internal = 'edit' (allows internal users to access)
|
||||
"""
|
||||
Document = self.env['documents.document']
|
||||
today = date.today()
|
||||
|
||||
# Root folder: ADP Billing Files (in Company workspace)
|
||||
root_folder = Document.search([
|
||||
('name', 'ilike', 'ADP Billing Files'),
|
||||
('type', '=', 'folder'),
|
||||
('folder_id', '=', False), # Root level folder
|
||||
], limit=1)
|
||||
|
||||
if not root_folder:
|
||||
root_folder = Document.sudo().create({
|
||||
'name': 'ADP Billing Files',
|
||||
'type': 'folder',
|
||||
'access_internal': 'edit', # Company workspace access
|
||||
'access_via_link': 'none',
|
||||
# Don't set company_id or owner_id - makes it a Company folder
|
||||
})
|
||||
|
||||
# Year folder
|
||||
year_name = str(today.year)
|
||||
year_folder = Document.search([
|
||||
('name', 'ilike', year_name),
|
||||
('type', '=', 'folder'),
|
||||
('folder_id', '=', root_folder.id),
|
||||
], limit=1)
|
||||
|
||||
if not year_folder:
|
||||
year_folder = Document.sudo().create({
|
||||
'name': year_name,
|
||||
'type': 'folder',
|
||||
'folder_id': root_folder.id,
|
||||
'access_internal': 'edit',
|
||||
'access_via_link': 'none',
|
||||
})
|
||||
|
||||
# Month folder
|
||||
month_name = today.strftime('%B')
|
||||
month_folder = Document.search([
|
||||
('name', 'ilike', month_name),
|
||||
('type', '=', 'folder'),
|
||||
('folder_id', '=', year_folder.id),
|
||||
], limit=1)
|
||||
|
||||
if not month_folder:
|
||||
month_folder = Document.sudo().create({
|
||||
'name': month_name,
|
||||
'type': 'folder',
|
||||
'folder_id': year_folder.id,
|
||||
'access_internal': 'edit',
|
||||
'access_via_link': 'none',
|
||||
})
|
||||
|
||||
return month_folder
|
||||
|
||||
def _get_or_create_folder_legacy(self):
|
||||
"""Get or create folders for older Odoo versions."""
|
||||
Documents = self.env['documents.document']
|
||||
company = self.env.company
|
||||
today = date.today()
|
||||
|
||||
# Root folder
|
||||
root_folder = Documents.search([
|
||||
('name', '=', 'ADP Billing Files'),
|
||||
('type', '=', 'folder'),
|
||||
('company_id', '=', company.id),
|
||||
], limit=1)
|
||||
|
||||
if not root_folder:
|
||||
root_folder = Documents.create({
|
||||
'name': 'ADP Billing Files',
|
||||
'type': 'folder',
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
# Year folder
|
||||
year_name = str(today.year)
|
||||
year_folder = Documents.search([
|
||||
('name', '=', year_name),
|
||||
('type', '=', 'folder'),
|
||||
('folder_id', '=', root_folder.id),
|
||||
], limit=1)
|
||||
|
||||
if not year_folder:
|
||||
year_folder = Documents.create({
|
||||
'name': year_name,
|
||||
'type': 'folder',
|
||||
'folder_id': root_folder.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
# Month folder
|
||||
month_name = today.strftime('%B')
|
||||
month_folder = Documents.search([
|
||||
('name', '=', month_name),
|
||||
('type', '=', 'folder'),
|
||||
('folder_id', '=', year_folder.id),
|
||||
], limit=1)
|
||||
|
||||
if not month_folder:
|
||||
month_folder = Documents.create({
|
||||
'name': month_name,
|
||||
'type': 'folder',
|
||||
'folder_id': year_folder.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
|
||||
return month_folder
|
||||
Reference in New Issue
Block a user