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

@@ -5,7 +5,7 @@
{
'name': 'Fusion Claims',
'version': '19.0.7.2.0',
'version': '19.0.7.3.0',
'category': 'Sales',
'summary': 'Complete ADP Claims Management with Dashboard, Sales Integration, Billing Automation, and Two-Stage Verification.',
'description': """
@@ -134,6 +134,7 @@
'views/dashboard_views.xml',
'views/client_profile_views.xml',
'wizard/xml_import_wizard_views.xml',
'views/adp_export_record_views.xml',
'views/adp_claims_views.xml',
'views/submission_history_views.xml',
'views/product_template_adp_views.xml',

View File

@@ -4,6 +4,7 @@
# Part of the Fusion Claim Assistant product family.
from . import adp_posting_schedule
from . import adp_export_record
from . import res_company
from . import res_config_settings
from . import fusion_central_config

View File

@@ -0,0 +1,344 @@
# -*- 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 io
import re
import base64
import logging
import zipfile
from datetime import date, datetime
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class ADPExportRecord(models.Model):
_name = 'fusion_claims.adp.export.record'
_description = 'ADP Export File Record'
_inherit = ['fusion_claims.adp.posting.schedule.mixin']
_order = 'export_date desc, id desc'
name = fields.Char(
string='Filename',
required=True,
index=True,
)
export_date = fields.Datetime(
string='Export Date',
required=True,
default=fields.Datetime.now,
index=True,
)
posting_period_date = fields.Date(
string='Posting Period',
required=True,
index=True,
help='The ADP posting date this export targets',
)
posting_period_label = fields.Char(
string='Posting Period Label',
compute='_compute_period_fields',
store=True,
)
year = fields.Char(
string='Year',
compute='_compute_period_fields',
store=True,
index=True,
)
month = fields.Char(
string='Month',
compute='_compute_period_fields',
store=True,
)
month_number = fields.Integer(
string='Month #',
compute='_compute_period_fields',
store=True,
help='Numeric month for proper ordering',
)
file_data = fields.Binary(
string='Export File',
attachment=True,
help='The exported ADP claims file (stored in filestore)',
)
filename = fields.Char(
string='Download Filename',
)
invoice_ids = fields.Many2many(
'account.move',
'adp_export_record_invoice_rel',
'export_id',
'invoice_id',
string='Exported Invoices',
)
invoice_count = fields.Integer(
string='Invoice Count',
compute='_compute_invoice_count',
)
line_count = fields.Integer(
string='Lines Exported',
default=0,
)
vendor_code = fields.Char(
string='Vendor Code',
)
user_id = fields.Many2one(
'res.users',
string='Exported By',
default=lambda self: self.env.uid,
index=True,
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
index=True,
)
notes = fields.Text(
string='Notes',
)
@api.depends('posting_period_date')
def _compute_period_fields(self):
for record in self:
ppd = record.posting_period_date
if ppd:
record.year = str(ppd.year)
record.month = ppd.strftime('%B')
record.month_number = ppd.month
record.posting_period_label = ppd.strftime('%b %d, %Y')
else:
record.year = ''
record.month = ''
record.month_number = 0
record.posting_period_label = ''
@api.depends('invoice_ids')
def _compute_invoice_count(self):
for record in self:
record.invoice_count = len(record.invoice_ids)
def action_download(self):
"""Download the export file."""
self.ensure_one()
if not self.file_data:
raise UserError(_("No file data available for download."))
return {
'type': 'ir.actions.act_url',
'url': f'/web/content?model={self._name}&id={self.id}'
f'&field=file_data&filename_field=filename&download=true',
'target': 'self',
}
def action_view_invoices(self):
"""Open the list of invoices included in this export."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Exported Invoices'),
'res_model': 'account.move',
'view_mode': 'list,form',
'domain': [('id', 'in', self.invoice_ids.ids)],
}
def action_download_zip(self):
"""Download selected export records as a single ZIP file.
Works as a multi-record action from the list view.
"""
if not self:
raise UserError(_("Please select at least one export record."))
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
for record in self:
if not record.file_data:
continue
file_content = base64.b64decode(record.file_data)
archive_name = record.filename or record.name
zf.writestr(archive_name, file_content)
zip_data = base64.b64encode(buf.getvalue())
buf.close()
# Create a transient attachment for download
today_str = date.today().strftime('%Y-%m-%d')
zip_filename = f"ADP_Export_Files_{today_str}.zip"
attachment = self.env['ir.attachment'].sudo().create({
'name': zip_filename,
'type': 'binary',
'datas': zip_data,
'mimetype': 'application/zip',
})
return {
'type': 'ir.actions.act_url',
'url': f'/web/content/{attachment.id}?download=true',
'target': 'self',
}
# ======================================================================
# MIGRATION: Documents app -> ADP Export Records
# ======================================================================
@api.model
def migrate_from_documents(self):
"""Migrate existing ADP export files from Documents app to this model.
Searches for binary files under 'ADP Billing Files' folder hierarchy,
parses filenames to determine vendor code and date, calculates the
posting period, and creates export records.
Returns a notification dict for the UI.
"""
if 'documents.document' not in self.env:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Documents App Not Installed'),
'message': _('The Documents app is not installed. Nothing to migrate.'),
'type': 'warning',
'sticky': False,
},
}
Document = self.env['documents.document'].sudo()
root_folders = Document.search([
('name', 'ilike', 'ADP Billing Files'),
('type', '=', 'folder'),
])
if not root_folders:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('No Files Found'),
'message': _('No "ADP Billing Files" folder found in Documents.'),
'type': 'info',
'sticky': False,
},
}
all_folder_ids = self._collect_subfolder_ids(Document, root_folders.ids)
all_folder_ids.extend(root_folders.ids)
files = Document.search([
('folder_id', 'in', all_folder_ids),
('type', '=', 'binary'),
])
if not files:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('No Files Found'),
'message': _('No export files found in ADP Billing Files folders.'),
'type': 'info',
'sticky': False,
},
}
migrated = 0
skipped = 0
errors = 0
for doc in files:
existing = self.search([('name', '=', doc.name)], limit=1)
if existing:
skipped += 1
continue
try:
vendor_code, file_date = self._parse_export_filename(doc.name)
if file_date:
posting_date = self._get_current_posting_date(file_date)
else:
posting_date = self._get_current_posting_date(
doc.create_date.date() if doc.create_date else date.today()
)
self.create({
'name': doc.name,
'filename': doc.name,
'file_data': doc.datas,
'export_date': doc.create_date or fields.Datetime.now(),
'posting_period_date': posting_date,
'vendor_code': vendor_code or '',
'user_id': doc.create_uid.id if doc.create_uid else self.env.uid,
'company_id': self.env.company.id,
'notes': f'Migrated from Documents app on {date.today()}',
})
migrated += 1
_logger.info("Migrated ADP export file: %s", doc.name)
except Exception as e:
errors += 1
_logger.error("Failed to migrate document %s: %s", doc.name, e)
# Archive migrated documents so they don't clutter the Documents app
if migrated > 0:
try:
migrated_docs = files.filtered(
lambda d: not self.search([('name', '=', d.name)], limit=1) is None
)
if hasattr(Document, 'active'):
files.write({'active': False})
_logger.info("Archived %d documents after migration", len(files))
except Exception as e:
_logger.warning("Could not archive old documents: %s", e)
msg = _(
"Migration complete: %d files migrated, %d skipped (already exist), %d errors."
) % (migrated, skipped, errors)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Migration Complete'),
'message': msg,
'type': 'success' if errors == 0 else 'warning',
'sticky': True,
},
}
@api.model
def _collect_subfolder_ids(self, Document, parent_ids):
"""Recursively collect all subfolder IDs under the given parent folders."""
all_ids = []
children = Document.search([
('folder_id', 'in', parent_ids),
('type', '=', 'folder'),
])
if children:
all_ids.extend(children.ids)
all_ids.extend(self._collect_subfolder_ids(Document, children.ids))
return all_ids
@api.model
def _parse_export_filename(self, filename):
"""Parse an ADP export filename to extract vendor code and date.
Expected format: VENDORCODE_YYYY-MM-DD.txt
Returns: (vendor_code, date_obj) or (None, None) if unparseable.
"""
if not filename:
return None, None
match = re.match(r'^(.+?)_(\d{4}-\d{2}-\d{2})\.\w+$', filename)
if match:
vendor_code = match.group(1)
try:
file_date = date.fromisoformat(match.group(2))
return vendor_code, file_date
except ValueError:
return vendor_code, None
return None, None

View File

@@ -641,3 +641,7 @@ class ResConfigSettings(models.TransientModel):
def action_set_gradient_dark_slate(self):
self._apply_gradient_preset('dark_slate')
def action_migrate_adp_export_files(self):
"""Migrate ADP export files from Documents app to the new Export Files menu."""
return self.env['fusion_claims.adp.export.record'].migrate_from_documents()

View File

@@ -2,6 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fusion_claims_config_user,fusion.central.config.user,model_fusion_claims_config,base.group_user,1,1,1,1
access_fusion_claims_export_wizard_user,fusion.central.export.wizard.user,model_fusion_claims_export_wizard,account.group_account_invoice,1,1,1,1
access_fusion_claims_export_wizard_manager,fusion.central.export.wizard.manager,model_fusion_claims_export_wizard,account.group_account_manager,1,1,1,1
access_fusion_adp_export_record_user,fusion.adp.export.record.user,model_fusion_claims_adp_export_record,account.group_account_invoice,1,0,1,0
access_fusion_adp_export_record_manager,fusion.adp.export.record.manager,model_fusion_claims_adp_export_record,account.group_account_manager,1,1,1,1
access_fusion_adp_device_code_user,fusion.adp.device.code.user,model_fusion_adp_device_code,base.group_user,1,0,0,0
access_fusion_adp_device_code_sales,fusion.adp.device.code.sales,model_fusion_adp_device_code,sales_team.group_sale_salesman,1,1,1,0
access_fusion_adp_device_code_manager,fusion.adp.device.code.manager,model_fusion_adp_device_code,sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fusion_claims_config_user fusion.central.config.user model_fusion_claims_config base.group_user 1 1 1 1
3 access_fusion_claims_export_wizard_user fusion.central.export.wizard.user model_fusion_claims_export_wizard account.group_account_invoice 1 1 1 1
4 access_fusion_claims_export_wizard_manager fusion.central.export.wizard.manager model_fusion_claims_export_wizard account.group_account_manager 1 1 1 1
5 access_fusion_adp_export_record_user fusion.adp.export.record.user model_fusion_claims_adp_export_record account.group_account_invoice 1 0 1 0
6 access_fusion_adp_export_record_manager fusion.adp.export.record.manager model_fusion_claims_adp_export_record account.group_account_manager 1 1 1 1
7 access_fusion_adp_device_code_user fusion.adp.device.code.user model_fusion_adp_device_code base.group_user 1 0 0 0
8 access_fusion_adp_device_code_sales fusion.adp.device.code.sales model_fusion_adp_device_code sales_team.group_sale_salesman 1 1 1 0
9 access_fusion_adp_device_code_manager fusion.adp.device.code.manager model_fusion_adp_device_code sales_team.group_sale_manager 1 1 1 1

View File

@@ -59,10 +59,11 @@
<field name="inherit_id" ref="account.view_move_form"/>
<field name="priority">52</field>
<field name="arch" type="xml">
<!-- Add Export and Sync buttons to header -->
<!-- Add Export and Verify buttons to header -->
<xpath expr="//button[@name='button_draft']" position="after">
<field name="x_fc_is_adp_invoice" invisible="1"/>
<field name="x_fc_needs_device_verification" invisible="1"/>
<field name="x_fc_adp_invoice_portion" invisible="1"/>
<!-- Verify Device Approval button - only on client invoices that need verification -->
<button name="action_open_device_approval_wizard"
string="Verify Device Approval"
@@ -71,19 +72,12 @@
icon="fa-check-square-o"
invisible="not x_fc_needs_device_verification"
help="Complete device verification to enable ADP invoice creation"/>
<button name="action_sync_to_sale_order"
string="Sync All"
type="object"
class="btn-secondary"
icon="fa-refresh"
invisible="move_type not in ['out_invoice', 'out_refund'] or not x_fc_is_adp_invoice"
help="Sync ADP fields from this Invoice to the Sale Order and all linked invoices"/>
<button name="action_export_adp_claim"
string="Export ADP"
type="object"
class="btn-secondary"
icon="fa-file-text-o"
invisible="move_type not in ['out_invoice', 'out_refund'] or state != 'posted' or not x_fc_is_adp_invoice"/>
invisible="move_type not in ['out_invoice', 'out_refund'] or state != 'posted' or not x_fc_is_adp_invoice or x_fc_adp_invoice_portion != 'adp'"/>
</xpath>
</field>
</record>

View File

@@ -1739,6 +1739,8 @@ else:
action="action_adp_invoices" sequence="2"/>
<menuitem id="menu_adp_client_invoices" name="ADP Client Invoices" parent="menu_fc_adp"
action="action_adp_client_invoices" sequence="3"/>
<menuitem id="menu_adp_export_files" name="Export Files" parent="menu_fc_adp"
action="action_adp_export_records" sequence="4"/>
<menuitem id="menu_adp_quotations"
name="Quotation Stage"

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2024-2025 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Claim Assistant product family.
-->
<odoo>
<!-- ===================================================================== -->
<!-- ADP EXPORT RECORD: Tree View -->
<!-- ===================================================================== -->
<record id="view_adp_export_record_list" model="ir.ui.view">
<field name="name">fusion_claims.adp.export.record.list</field>
<field name="model">fusion_claims.adp.export.record</field>
<field name="arch" type="xml">
<list string="ADP Export Files" default_order="export_date desc">
<field name="name"/>
<field name="export_date"/>
<field name="posting_period_label" string="Posting Period"/>
<field name="vendor_code"/>
<field name="line_count" string="Lines"/>
<field name="invoice_count" string="Invoices"/>
<field name="user_id" string="Exported By" widget="many2one_avatar_user"/>
<field name="year" column_invisible="True"/>
<field name="month" column_invisible="True"/>
<field name="month_number" column_invisible="True"/>
</list>
</field>
</record>
<!-- ===================================================================== -->
<!-- ADP EXPORT RECORD: Form View -->
<!-- ===================================================================== -->
<record id="view_adp_export_record_form" model="ir.ui.view">
<field name="name">fusion_claims.adp.export.record.form</field>
<field name="model">fusion_claims.adp.export.record</field>
<field name="arch" type="xml">
<form string="ADP Export File">
<header>
<button name="action_download" string="Download File"
type="object" class="btn-primary" icon="fa-download"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group string="Export Details">
<field name="export_date" readonly="1"/>
<field name="posting_period_date" readonly="1"/>
<field name="posting_period_label" readonly="1"/>
<field name="vendor_code" readonly="1"/>
<field name="line_count" readonly="1"/>
</group>
<group string="File">
<field name="file_data" filename="filename" readonly="1"/>
<field name="filename" invisible="1"/>
<field name="user_id" readonly="1"/>
<field name="company_id" readonly="1" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Exported Invoices" name="invoices">
<field name="invoice_ids" readonly="1" nolabel="1">
<list string="Invoices" create="0" delete="0">
<field name="name" string="Invoice"/>
<field name="partner_id" string="Customer"/>
<field name="invoice_date"/>
<field name="amount_total" string="Total"/>
<field name="state" widget="badge"
decoration-success="state == 'posted'"
decoration-info="state == 'draft'"/>
</list>
</field>
</page>
<page string="Notes" name="notes" invisible="not notes">
<field name="notes" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- ===================================================================== -->
<!-- ADP EXPORT RECORD: Search View -->
<!-- ===================================================================== -->
<record id="view_adp_export_record_search" model="ir.ui.view">
<field name="name">fusion_claims.adp.export.record.search</field>
<field name="model">fusion_claims.adp.export.record</field>
<field name="arch" type="xml">
<search string="ADP Export Files">
<field name="name"/>
<field name="vendor_code"/>
<field name="user_id"/>
<separator/>
<group expand="1" string="Group By">
<filter string="Year" name="group_year"
context="{'group_by': 'year'}"/>
<filter string="Month" name="group_month"
context="{'group_by': 'month'}"/>
<filter string="Posting Period" name="group_posting"
context="{'group_by': 'posting_period_date'}"/>
<filter string="Exported By" name="group_user"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<!-- ===================================================================== -->
<!-- ADP EXPORT RECORD: Action -->
<!-- ===================================================================== -->
<record id="action_adp_export_records" model="ir.actions.act_window">
<field name="name">Export Files</field>
<field name="res_model">fusion_claims.adp.export.record</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_adp_export_record_search"/>
<field name="context">{'search_default_group_year': 1, 'search_default_group_month': 1, 'search_default_group_posting': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No export files yet
</p>
<p>
ADP export files will appear here after you export invoices using the
<strong>Export ADP</strong> button on ADP portion invoices.
</p>
</field>
</record>
<!-- ===================================================================== -->
<!-- ADP EXPORT RECORD: Server Action for Bulk ZIP Download -->
<!-- ===================================================================== -->
<record id="action_adp_export_download_zip" model="ir.actions.server">
<field name="name">Download as ZIP</field>
<field name="model_id" ref="model_fusion_claims_adp_export_record"/>
<field name="binding_model_id" ref="model_fusion_claims_adp_export_record"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">action = records.action_download_zip()</field>
</record>
</odoo>

View File

@@ -330,6 +330,30 @@
</div>
</div>
<!-- ============================================================= -->
<!-- DATA MIGRATION -->
<!-- ============================================================= -->
<h2>Data Migration</h2>
<div class="row mt-4 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Migrate Export Files from Documents</span>
<div class="text-muted">
Move existing ADP export files from the Documents app into the
new ADP Export Files menu. Only needs to be run once after upgrading.
</div>
<div class="mt-2">
<button name="action_migrate_adp_export_files"
string="Migrate Export Files"
type="object"
class="btn-secondary"
icon="fa-exchange"
confirm="This will migrate all ADP export files from Documents to the new Export Files menu. Continue?"/>
</div>
</div>
</div>
</div>
<!-- ============================================================= -->
<!-- ODSP CONFIGURATION -->
<!-- ============================================================= -->

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>