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:
@@ -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
|
||||
|
||||
@@ -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 > ADP > 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>
|
||||
|
||||
Reference in New Issue
Block a user