feat: ADP Export Files menu with filestore storage, remove Sync All button

- Add fusion_claims.adp.export.record model with filestore-backed Binary field
  for tracking exported ADP claims files organized by Year > Month > Posting Period
- Add tree/form/search views with default group-by hierarchy, latest first
- Add "Export Files" menuitem under ADP menu section
- Add bulk ZIP download server action for multi-select export
- Replace Documents app storage with new model in export wizard
- Remove Documents-related methods (_save_to_documents, folder creation)
- Add migration button in Settings to move existing Documents files
- Fix Export ADP button visibility: only show on ADP portion invoices
- Remove redundant Sync All button from invoice form
- Add ACL entries for billing users (read/create) and managers (full CRUD)
- Bump version to 19.0.7.3.0

Made-with: Cursor
This commit is contained in:
gsinghpal
2026-03-15 12:27:06 -04:00
parent 0e04f4ecc6
commit a839285bd4
11 changed files with 578 additions and 229 deletions

View File

@@ -39,7 +39,7 @@ class FusionCentralExportWizard(models.TransientModel):
('done', 'Done'),
], default='draft')
export_summary = fields.Text(string='Export Summary', readonly=True)
saved_to_documents = fields.Boolean(string='Saved to Documents', readonly=True)
saved_to_export_records = fields.Boolean(string='Saved to Export Files', readonly=True)
warnings = fields.Text(string='Warnings', readonly=True)
@api.model
@@ -343,110 +343,80 @@ class FusionCentralExportWizard(models.TransientModel):
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.
"""Check if a file with the same name already exists in ADP Export Records.
Returns:
tuple: (exists: bool, existing_files: list of names)
tuple: (exists: bool, existing_files: list of description strings)
"""
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))
ExportRecord = self.env['fusion_claims.adp.export.record']
existing = ExportRecord.search([('name', '=', filename)])
if existing:
existing_files = [
f"{rec.name} (exported: {rec.export_date.strftime('%Y-%m-%d %H:%M')})"
for rec in existing
]
return True, existing_files
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
4. Save to ADP Export Records (filestore-backed)
5. Show download window
"""
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"WARNING: A file with the name '{filename}' already exists in Export Files.\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
saved = self._save_to_export_records(filename, file_data, line_count)
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
summary += "\n" + _("File saved to Fusion Claims > ADP > Export Files.")
self.write({
'export_file': file_data,
'export_filename': filename,
'state': 'done',
'export_summary': summary,
'saved_to_documents': saved,
'saved_to_export_records': saved,
'warnings': warnings_text,
})
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
@@ -455,163 +425,28 @@ class FusionCentralExportWizard(models.TransientModel):
'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
def _save_to_export_records(self, filename, file_data, line_count):
"""Save export file to the ADP Export Records model (filestore-backed)."""
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({
ExportRecord = self.env['fusion_claims.adp.export.record']
posting_date = ExportRecord._get_current_posting_date(self.export_date)
ExportRecord.create({
'name': filename,
'type': 'binary',
'datas': base64.b64encode(content.encode('utf-8')),
'folder_id': folder.id,
'access_internal': 'edit', # Allow internal users to access
'filename': filename,
'file_data': file_data,
'export_date': fields.Datetime.now(),
'posting_period_date': posting_date,
'vendor_code': self.vendor_code,
'line_count': line_count,
'invoice_ids': [(6, 0, self.invoice_ids.ids)],
'user_id': self.env.uid,
'company_id': self.env.company.id,
})
_logger.info("Saved ADP export to Documents: %s", filename)
_logger.info("Saved ADP export record: %s", filename)
return True
except Exception as e:
_logger.warning("Could not save to Documents: %s", str(e))
_logger.error("Could not save ADP export record: %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

View File

@@ -40,13 +40,13 @@
<field name="export_file" filename="export_filename" readonly="1"/>
</group>
<div class="text-muted small" invisible="not saved_to_documents">
<i class="fa fa-folder-open"/> File also saved to Documents app
<div class="text-muted small" invisible="not saved_to_export_records">
<i class="fa fa-folder-open"/> File saved to Fusion Claims &gt; ADP &gt; Export Files
</div>
</group>
<field name="state" invisible="1"/>
<field name="saved_to_documents" invisible="1"/>
<field name="saved_to_export_records" invisible="1"/>
<field name="warnings" invisible="1"/>
<footer>