Compare commits

..

60 Commits

Author SHA1 Message Date
Deepak 41f493840b docker files added 2026-06-10 16:53:14 +05:30
Bhagya-K 54541998c5 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-10 12:00:05 +05:30
Bhagya-K 68e62956b7 resolved installation issue of grace period module 2026-06-10 11:59:42 +05:30
seshikanth 14e725be4f #fix: Employee Performance Management Module 2026-06-09 13:31:02 +05:30
Bhagya-K adfe801d8e Roster management and grace period changes 2026-06-08 17:01:09 +05:30
Bhagya-K 5c6341d8b7 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module
# Conflicts:
#	addons_extensions/grace_period/models/__init__.py
2026-06-08 16:33:01 +05:30
Bhagya-K 71f416e8e8 grace Period 2026-06-08 16:30:00 +05:30
pranaysaidurga b5f643fb18 attendance, leaves, weekly timesheets enhancements 2026-06-05 16:42:23 +05:30
karuna 3092032fee enhancements of PMT 2026-06-05 15:16:49 +05:30
karuna f118600ab6 enhancements of PMT 2026-06-04 15:36:32 +05:30
karuna 05bdddc472 enhancements of PMT 2026-06-04 11:06:43 +05:30
pranaysaidurga 582225e11e employee issue 2026-06-04 10:51:17 +05:30
pranaysaidurga 478c1708fb Auto doc get default recruitment 2026-06-04 10:43:54 +05:30
pranaysaidurga 945aedc0b4 Recruitement Dashboards 2026-06-03 14:23:00 +05:30
pranaysaidurga eb17d717dd Project Changes 2026-06-03 10:55:50 +05:30
pranaysaidurga 604d556501 Recruitment changes 2026-06-03 10:48:17 +05:30
karuna 90211776a1 small enhancements 2026-05-28 12:20:45 +05:30
karuna 923304f759 project enhancements 2026-05-26 15:03:48 +05:30
pranaysaidurga 96493be796 Leaves Timesheets Management 2026-05-25 16:38:40 +05:30
pranaysaidurga 5460f6c207 Employee it declaration 2026-05-25 16:10:45 +05:30
pranaysaidurga 4db7e5ade2 Document parser upload 2026-05-20 18:59:16 +05:30
pranaysaidurga f2788e025d bench management system changes 2026-05-19 16:10:05 +05:30
pranaysaidurga 0e51ac85e9 user_ids in project tasks bug 2026-05-19 13:48:45 +05:30
pranaysaidurga 19e5f1db80 Project, recruitment, payroll, bench management changes and updates 2026-05-19 13:36:24 +05:30
karuna c2e33753bb project modifications 2026-05-19 12:08:41 +05:30
pranaysaidurga f6bfd46f2c my tasks issue fix 2026-05-14 12:23:28 +05:30
pranaysaidurga c9746456b8 equipment extended 2026-05-13 11:28:50 +05:30
pranaysaidurga b0ec5ee508 Menu Control Center Functionality change 2026-05-12 11:37:01 +05:30
pranaysaidurga e2d6a8c417 user own estimations 2026-05-07 14:14:41 +05:30
pranaysaidurga 9c33507a45 multi company changes 2026-05-06 18:17:04 +05:30
pranaysaidurga a9a6c683d7 new timeline feature and bug fixes 2026-05-06 17:26:15 +05:30
pranaysaidurga 723dcbe225 portfolio company rules 2026-05-06 11:51:28 +05:30
pranaysaidurga 4139e5fa33 company_id constrain fix 2026-05-06 10:56:13 +05:30
pranaysaidurga ce93d9601c project updates and changes 2026-05-05 11:55:32 +05:30
pranaysaidurga 1b21175e75 accounting kit 2026-05-04 10:56:10 +05:30
pranaysaidurga 74526cc1a2 project task issue fix and changes 2026-04-27 16:14:25 +05:30
pranaysaidurga 6a32ac3f37 project task creation issue 2026-04-27 15:27:27 +05:30
pranaysaidurga fa3833bac3 Project Employee view 2026-04-27 12:37:57 +05:30
pranay 73a27d8921 master selector fix 2026-04-14 11:45:11 +05:30
pranay 26923e20b9 bug fix 2026-04-13 18:21:55 +05:30
karuna ff806505e2 PMT bug fix 2026-04-13 17:56:48 +05:30
karuna b5b276f552 Module Master Selector 2026-03-10 10:45:00 +05:30
Pranay 5d6c2c09aa list view scrolling issue 2026-02-05 12:23:55 +05:30
Pranay 4b85cc0f59 Recruitment changes, Menu Contral Center xml change, attachment preview changes 2026-02-04 11:57:31 +05:30
Pranay ad5967d420 ica web responsive theme 2025-12-24 17:50:22 +05:30
Pranay 53f90a7834 packages list 2025-12-24 17:29:23 +05:30
Pranay 87824199d0 project management system Commit 2025-12-24 12:50:57 +05:30
karuna 92543295d6 adding page visibility based on stages 2025-12-15 18:07:11 +05:30
Pranay 7b6d108ace one2many search widget and project task module updates 2025-12-15 11:25:18 +05:30
Pranay 6f77059f85 PMT, UNIVERSAL ATTACHMENT PREVIEW, MENU CONTROL CENTER INTEGRATION 2025-12-10 10:09:23 +05:30
Pranay c93d208990 Document Preview 2025-12-03 10:19:21 +05:30
Pranay bfd7890cbc Documents preview, binary field widget, menu control center changes 2025-12-03 10:16:19 +05:30
karuna d362ef87aa kudos changes commit 2025-12-03 10:11:32 +05:30
Pranay 20d22c1f04 Project timesheet updates 2025-11-25 16:45:09 +05:30
pranay 44e5ee7e2f ALL MODULE CHANGES 2025-11-20 10:11:59 +05:30
pranay 0fa84c6d43 PMS Updates 2025-11-20 10:06:10 +05:30
karuna 4ce02e58fa Rewards Module for PMS 2025-11-13 12:43:31 +05:30
pranay 66077d1819 Project Task Extended Changes 2025-11-13 12:36:54 +05:30
karuna 2dbdb58127 internal team model update 2025-11-10 10:14:09 +05:30
pranay 5f267e96da PROJECT MODULE AND THEME ADDED IN SHARED MODULES 2025-11-04 11:57:04 +05:30
3015 changed files with 1420466 additions and 1292309 deletions

54
Dockerfile Normal file
View File

@ -0,0 +1,54 @@
FROM python:3.12-bookworm
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# System dependencies
RUN apt-get update && apt-get install -y \
build-essential \
gcc \
g++ \
git \
curl \
npm \
libpq-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxslt1-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libffi-dev \
libssl-dev \
liblcms2-dev \
wkhtmltopdf \
&& rm -rf /var/lib/apt/lists/*
# Less compiler used by Odoo
RUN npm install -g less less-plugin-clean-css
# Create odoo user
RUN useradd -m -d /opt/odoo -U -r -s /bin/bash odoo
WORKDIR /opt/odoo/odoo18
# Copy project
COPY . .
# Upgrade pip tools
RUN pip install --upgrade pip setuptools wheel
# Install requirements
RUN pip install --no-cache-dir -r requirements.txt
# PostgreSQL driver
RUN pip install psycopg2-binary
# Permissions
RUN chown -R odoo:odoo /opt/odoo
USER odoo
EXPOSE 8069
CMD ["python3", "odoo-bin", "-c", "odoo.conf"]

View File

@ -103,7 +103,7 @@ class ImBus(models.Model):
"""Low-level method to send ``notification_type`` and ``message`` to ``target``.
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
security.
security.
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
attacker.

View File

@ -6,7 +6,7 @@
'version': '1.0',
'category': 'Accounting/Localizations/Point of Sale',
'description': """
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability,security, storage and archiving of data related to sales to private individuals (B2C).
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Install it if you use the Point of Sale app to sell to individuals.

View File

@ -41,7 +41,7 @@ class Survey(http.Controller):
def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True):
""" Check survey is open and can be taken. This does not checks for
security rules, only functional / business rules. It returns a string key
security rules, only functional / business rules. It returns a string key
allowing further manipulation of validity issues
* survey_wrong: survey does not exist;

View File

@ -24,7 +24,7 @@
},
'installable': True,
'data': [
# security.xml first, data.xml need the group to exist (checking it)
#security.xml first, data.xml need the group to exist (checking it)
'security/website_security.xml',
'security/ir.model.access.csv',
'data/image_library.xml',

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,24 @@
{
'name': 'Bench Management',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Bench Management System',
'author': 'Team Srivyn',
'depends': [
'hr',
'project',
'hr_timesheet',
'project_task_timesheet_extended',
'hr_employee_extended'
],
'data': [
'security/ir.model.access.csv',
'data/sync_team_lines.xml',
'views/project.xml',
'views/bench_management_view.xml',
],
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<function model="project.project" name="_sync_all_team_lines_from_members"/>
</odoo>

View File

@ -0,0 +1,2 @@
from . import project
from . import bench_management

View File

@ -0,0 +1,174 @@
from odoo import models, fields, tools, api
class BenchManagementLine(models.Model):
_name = "bench.management.line"
_description = "Employee Availability (Bench)"
_auto = False
_rec_name = 'employee_id'
employee_id = fields.Many2one("hr.employee", readonly=True)
job_id = fields.Many2one("hr.job", readonly=True)
company_id = fields.Many2one("res.company", related="employee_id.company_id")
project_line_ids = fields.Many2many(
'project.team.line',
compute='_compute_bench_details',
string='Project Assignments',
readonly=True,
)
limited_project_line_ids = fields.Many2many(
compute='_compute_bench_details',
comodel_name='project.team.line',
string='Kanban Projects',
readonly=True,
)
project_names_tooltip = fields.Text(
string="Project Names",
compute='_compute_bench_details',
readonly=True,
)
project_count = fields.Integer(
string="Project Count",
compute='_compute_bench_details',
readonly=True,
)
active_project_count = fields.Integer(
string="Active Projects",
compute='_compute_bench_details',
readonly=True,
)
future_project_count = fields.Integer(
string="Upcoming Projects",
compute='_compute_bench_details',
readonly=True,
)
completed_project_count = fields.Integer(
string="Completed Projects",
compute='_compute_bench_details',
readonly=True,
)
status = fields.Selection([
("bench", "Bench"),
("partial", "Partial"),
("full", "Full"),
], readonly=True)
def _get_line_availability_status(self, line, today):
return line.status or 'not_started'
def _compute_bench_details(self):
project_team_line = self.env['project.team.line'].sudo()
today = fields.Date.context_today(self)
for rec in self:
project_lines = project_team_line.search(
[('employee_id', '=', rec.employee_id.id)],
order='start_date desc, id desc'
)
active_lines = project_lines.filtered(
lambda line: rec._get_line_availability_status(line, today) == 'in_progress'
)
future_lines = project_lines.filtered(
lambda line: rec._get_line_availability_status(line, today) == 'not_started'
)
completed_lines = project_lines.filtered(
lambda line: rec._get_line_availability_status(line, today) == 'done'
)
project_records = project_lines.mapped('project_id')
if active_lines:
bench_status = 'full'
elif future_lines:
bench_status = 'partial'
else:
bench_status = 'bench'
rec.project_line_ids = project_lines
rec.limited_project_line_ids = project_lines[:3]
rec.project_count = len(project_records)
rec.active_project_count = len(active_lines)
rec.future_project_count = len(future_lines)
rec.completed_project_count = len(completed_lines)
rec.project_names_tooltip = '\n'.join(
f"{line.project_id.display_name or 'No Project'} - {dict(line._fields['status'].selection).get(rec._get_line_availability_status(line, today), 'N/A')}"
for line in project_lines
) or ''
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
CREATE OR REPLACE VIEW bench_management_line AS (
SELECT
he.id AS id,
he.id AS employee_id,
he.job_id AS job_id,
CASE
WHEN EXISTS (
SELECT 1
FROM project_team_line tpl
WHERE tpl.employee_id = he.id
AND tpl.status = 'in_progress'
) THEN 'full'
WHEN EXISTS (
SELECT 1
FROM project_team_line tpl
WHERE tpl.employee_id = he.id
AND tpl.status = 'not_started'
) THEN 'partial'
ELSE 'bench'
END AS status
FROM hr_employee he
)
""")
class ProjectTeamLine(models.Model):
_inherit = 'project.team.line'
line_status_color = fields.Integer(
compute='_compute_line_status_color',
string='Status Color',
readonly=True,
)
@api.depends('status')
def _compute_line_status_color(self):
color_map = {
'not_started': 8,
'in_progress': 2,
'done': 10,
}
for line in self:
line.line_status_color = color_map.get(line.status, 0)
def name_get(self):
result = []
for rec in self:
name = rec.project_id.display_name or 'No Project'
result.append((rec.id, name))
return result
def _sync_project_members(self):
if self.env.context.get('skip_project_team_member_sync'):
return True
self.mapped('project_id')._sync_members_from_team_lines()
return True
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records._sync_project_members()
return records
def write(self, vals):
projects = self.mapped('project_id')
res = super().write(vals)
(projects | self.mapped('project_id'))._sync_members_from_team_lines()
return res
def unlink(self):
projects = self.mapped('project_id')
res = super().unlink()
projects._sync_members_from_team_lines()
return res

View File

@ -0,0 +1,223 @@
from odoo import Command, api, fields, models, _
from odoo.exceptions import ValidationError
class ProjectProject(models.Model):
_inherit = 'project.project'
team_line_ids = fields.One2many(
'project.team.line',
'project_id',
string="Team Details"
)
can_manage_team_lines = fields.Boolean(
compute='_compute_can_manage_team_lines',
string='Can Manage Team Lines'
)
@api.depends('user_id', 'project_lead')
def _compute_can_manage_team_lines(self):
current_user = self.env.user
for project in self:
project.can_manage_team_lines = bool(
self.env.is_superuser()
or project.user_id == current_user
or ('project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager"))
)
@api.onchange('team_line_ids')
def _onchange_team_line_ids(self):
for project in self:
users = project.team_line_ids.mapped('user_id')
project.members_ids = [(6, 0, users.ids)]
def _sync_members_from_team_lines(self):
if self.env.context.get('skip_project_team_member_sync'):
return
for project in self:
users = project.team_line_ids.mapped('user_id')
if set(project.members_ids.ids) != set(users.ids):
project.with_context(skip_project_team_member_sync=True).sudo().write({
'members_ids': [Command.set(users.ids)],
})
def _sync_team_lines_from_members(self):
if self.env.context.get('skip_project_team_member_sync'):
return
TeamLine = self.env['project.team.line'].sudo().with_context(skip_project_team_member_sync=True)
for project in self.sudo():
member_ids = set(project.members_ids.ids)
kept_user_ids = set()
lines_to_remove = self.env['project.team.line'].sudo()
for line in project.team_line_ids.sorted('id'):
user_id = line.user_id.id
if not user_id or user_id not in member_ids or user_id in kept_user_ids:
lines_to_remove |= line
else:
kept_user_ids.add(user_id)
if lines_to_remove:
lines_to_remove.with_context(skip_project_team_member_sync=True).unlink()
for user_id in member_ids - kept_user_ids:
TeamLine.create({
'project_id': project.id,
'user_id': user_id,
})
@api.model
def _sync_all_team_lines_from_members(self):
self.search([])._sync_team_lines_from_members()
return True
@api.model_create_multi
def create(self, vals_list):
projects = super().create(vals_list)
for project, vals in zip(projects, vals_list):
if 'team_line_ids' in vals:
project._sync_members_from_team_lines()
elif 'members_ids' in vals:
project._sync_team_lines_from_members()
return projects
def write(self, vals):
res = super().write(vals)
if 'team_line_ids' in vals:
self._sync_members_from_team_lines()
elif 'members_ids' in vals:
self._sync_team_lines_from_members()
return res
class ProjectTeamLine(models.Model):
_name = 'project.team.line'
_description = 'Project Team Line'
_rec_name = 'project_id'
project_id = fields.Many2one('project.project', ondelete='cascade')
user_id = fields.Many2one('res.users')
employee_id = fields.Many2one(
'hr.employee',
compute="_compute_employee",
store=True
)
job_id = fields.Many2one(
'hr.job',
related='employee_id.job_id',
store=True
)
start_date = fields.Date()
end_date = fields.Date()
status = fields.Selection([
('not_started', 'Not Started'),
('in_progress', 'In Progress'),
('done', 'Completed')
], compute='_compute_status', inverse='_inverse_status', store=True, readonly=False)
can_edit_assignment = fields.Boolean(
compute='_compute_can_edit_assignment',
string='Can Edit Assignment'
)
# ------------------------
# COMPUTE EMPLOYEE
# ------------------------
@api.depends('user_id')
def _compute_employee(self):
for rec in self:
rec.employee_id = self.env['hr.employee'].search([
('user_id', '=', rec.user_id.id)
], limit=1)
@api.depends('start_date', 'end_date')
def _compute_status(self):
today = fields.Date.context_today(self)
for rec in self:
if rec.end_date and rec.end_date < today:
rec.status = 'done'
elif rec.start_date and rec.start_date > today:
rec.status = 'not_started'
else:
rec.status = 'in_progress'
@api.depends('project_id.user_id', 'project_id.project_lead')
def _compute_can_edit_assignment(self):
current_user = self.env.user
for rec in self:
project = rec.project_id
rec.can_edit_assignment = bool(
self.env.is_superuser()
or (project and project.user_id == current_user)
or (project and 'project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager"))
)
def _inverse_status(self):
# Allow manual edits to the stored computed field.
# When start/end dates change later, compute will refresh it again.
return True
def _check_manager_access(self):
if self.env.is_superuser():
return
unauthorized = self.filtered(lambda rec: not rec.can_edit_assignment)
if unauthorized:
raise ValidationError(_("Only the related project manager can update team assignment dates or status."))
# ------------------------
# SYNC BENCH
# ------------------------
def _sync_bench(self):
# Bench data is read live from SQL view / computed fields,
# so there is no separate sync model to refresh here.
return True
# ------------------------
# CREATE
# ------------------------
@api.model_create_multi
def create(self, vals_list):
if not self.env.is_superuser():
for vals in vals_list:
project_id = vals.get('project_id')
if project_id:
project = self.env['project.project'].browse(project_id)
if not (
project.user_id == self.env.user
or ('project_lead' in project._fields and project.project_lead == self.env.user)
):
raise ValidationError(_("Only the related project manager can add team assignments."))
records = super().create(vals_list)
records._sync_bench()
records.mapped('project_id')._sync_members_from_team_lines()
return records
# ------------------------
# WRITE
# ------------------------
def write(self, vals):
if any(key in vals for key in ('status', 'start_date', 'end_date', 'user_id', 'project_id')):
self._check_manager_access()
projects = self.mapped('project_id')
res = super().write(vals)
self._sync_bench()
if any(key in vals for key in ('user_id', 'project_id')):
(projects | self.mapped('project_id'))._sync_members_from_team_lines()
return res
# ------------------------
# UNLINK
# ------------------------
def unlink(self):
projects = self.mapped('project_id')
self._check_manager_access()
res = super().unlink()
self._sync_bench()
projects._sync_members_from_team_lines()
return res

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
access_project_team_line,project.team.line,model_project_team_line,,1,1,1,1
access_bench_management_line,bench.management.line,model_bench_management_line,,1,1,1,1
1 id name model_id:id group_id perm_read perm_write perm_create perm_unlink
2 access_project_team_line project.team.line model_project_team_line 1 1 1 1
3 access_bench_management_line bench.management.line model_bench_management_line 1 1 1 1

View File

@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_bench_management_tree" model="ir.ui.view">
<field name="name">bench.management.line.list</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<list string="Bench Management">
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
<field name="limited_project_line_ids" widget="many2many_tags" options="{'color_field': 'line_status_color'}"/>
<field name="active_project_count"/>
<field name="future_project_count"/>
<field name="completed_project_count"/>
</list>
</field>
</record>
<record id="view_bench_management_search" model="ir.ui.view">
<field name="name">bench.management.line.search</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<search string="Bench Management">
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
<filter name="filter_bench" string="Bench" domain="[('status', '=', 'bench')]"/>
<filter name="filter_partial" string="Partially Available" domain="[('status', '=', 'partial')]"/>
<filter name="filter_full" string="Fully Allocated" domain="[('status', '=', 'full')]"/>
<group expand="0" string="Group By">
<filter name="group_by_status" string="Status" context="{'group_by': 'status'}"/>
<filter name="group_by_job" string="Job Position" context="{'group_by': 'job_id'}"/>
</group>
</search>
</field>
</record>
<record id="view_bench_management_form" model="ir.ui.view">
<field name="name">bench.management.line.form</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<form string="Bench Management">
<sheet>
<group>
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
</group>
<group string="Project Information">
<field name="project_line_ids" nolabel="1" readonly="0">
<list create="0" delete="0" editable="bottom">
<field name="project_id" readonly="1"/>
<field name="can_edit_assignment" column_invisible="1"/>
<field name="status" readonly="not can_edit_assignment"/>
<field name="start_date" readonly="not can_edit_assignment"/>
<field name="end_date" readonly="not can_edit_assignment"/>
<field name="job_id" optional="hide" readonly="1"/>
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_bench_management_kanban" model="ir.ui.view">
<field name="name">bench.management.line.kanban</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
<field name="limited_project_line_ids"/>
<field name="project_count"/>
<field name="active_project_count"/>
<field name="future_project_count"/>
<field name="completed_project_count"/>
<field name="project_names_tooltip"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click"
style="border-radius:16px;border:1px solid #dbe4ee;background:linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);padding:16px;min-height:260px;box-shadow:0 8px 24px rgba(15, 23, 42, 0.06);">
<!-- Header -->
<div class="d-flex align-items-center mb-3">
<img t-att-src="'/web/image/hr.employee/' + record.employee_id.raw_value + '/avatar_128'"
style="
width:42px;
height:42px;
border-radius:50%;
object-fit:cover;
margin-right:10px;
"/>
<div>
<div style="
font-size:16px;
font-weight:600;
color:#1f2937;
">
<field name="employee_id"/>
</div>
<div style="
font-size:12px;
color:#6b7280;
">
<field name="job_id"/>
</div>
</div>
</div>
<!-- Status -->
<div class="mb-3 d-flex align-items-center justify-content-between">
<t t-if="record.status.raw_value == 'bench'">
<span style="
background:#e2e8f0;
color:#334155;
padding:5px 10px;
border-radius:20px;
font-size:11px;
font-weight:600;
">
Bench
</span>
</t>
<t t-if="record.status.raw_value == 'partial'">
<span style="
background:#fef3c7;
color:#92400e;
padding:5px 10px;
border-radius:20px;
font-size:11px;
font-weight:600;
">
Partially Available
</span>
</t>
<t t-if="record.status.raw_value == 'full'">
<span style="
background:#dcfce7;
color:#166534;
padding:5px 10px;
border-radius:20px;
font-size:11px;
font-weight:600;
">
Fully Allocated
</span>
</t>
<div style="font-size:11px;color:#64748b;">
<t t-out="record.active_project_count.raw_value"/> Active
</div>
</div>
<div class="d-flex gap-2 mb-3" style="gap:8px;">
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Current</div>
<div style="font-size:18px;font-weight:700;color:#0f172a;">
<t t-out="record.active_project_count.raw_value"/>
</div>
</div>
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Upcoming</div>
<div style="font-size:18px;font-weight:700;color:#0f172a;">
<t t-out="record.future_project_count.raw_value"/>
</div>
</div>
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Completed</div>
<div style="font-size:18px;font-weight:700;color:#0f172a;">
<t t-out="record.completed_project_count.raw_value"/>
</div>
</div>
</div>
<!-- Project Info -->
<div t-if="record.project_count.raw_value"
style="border-top:1px solid #f3f4f6;padding-top:12px;">
<div style="
font-size:13px;
font-weight:600;
color:#374151;
margin-bottom:8px;
">
Projects
</div>
<div t-att-title="record.project_names_tooltip.raw_value"
style="max-height:48px;overflow:hidden;">
<field name="limited_project_line_ids"
widget="many2many_tags"
options="{'no_create': True, 'no_create_edit': True, 'no_open': True, 'color_field': 'line_status_color'}"/>
</div>
<t t-if="record.project_count.raw_value &gt; 3">
<div t-att-title="record.project_names_tooltip.raw_value"
style="margin-top:8px;font-size:11px;color:#2563eb;font-weight:600;">
+ <t t-out="record.project_count.raw_value - 3"/> more
</div>
</t>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_bench_management" model="ir.actions.act_window">
<field name="name">Bench Management</field>
<field name="res_model">bench.management.line</field>
<field name="view_mode">kanban,list,form</field>
<field name="domain">[('company_id', 'in', allowed_company_ids)]</field>
<field name="search_view_id" ref="view_bench_management_search"/>
</record>
<menuitem id="menu_bench_management"
name="Employee Bench"
parent="hr.menu_hr_root"
action="action_bench_management"
sequence="3"/>
</odoo>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_project_inherit_form_view2_inherit" model="ir.ui.view">
<field name="name">project.project.inherit.form.view.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<field name="can_manage_team_lines" invisible="1"/>
</xpath>
<xpath expr="//page[@name='team']/group[field[@name='members_ids']]" position="after">
<group string="Project Team Details">
<field name="team_line_ids" nolabel="1" readonly="not can_manage_team_lines">
<list editable="bottom">
<field name="user_id" string="Employee Name"/>
<field name="employee_id" string="Employee" readonly="1" column_invisible="1" invisible="1"/>
<field name="job_id" string="Job Position"/>
<field name="project_id" column_invisible="1"/>
<field name="can_edit_assignment" column_invisible="1"/>
<field name="start_date" string="Start Date" readonly="not can_edit_assignment"/>
<field name="end_date" string="End date" readonly="not can_edit_assignment"/>
<field name="status" string="Status" readonly="not can_edit_assignment"/>
</list>
</field>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -1 +1 @@
from . import models
from . import models

View File

@ -1,21 +1,21 @@
{
'name': 'Consolidated Payslip Grid (OWL)',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
'author': 'Raman Marikanti',
'depends': ['hr_payroll', 'web'],
'data': [
'security/ir.model.access.csv',
'views/batch_payslip_view.xml',
],
'assets': {
'web.assets_backend': [
# Internal module JS and XML files (ensure correct paths within 'static/src')
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js',
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
],
},
'installable': True,
}
{
'name': 'Consolidated Payslip Grid (OWL)',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
'author': 'Raman Marikanti',
'depends': ['hr_payroll', 'web'],
'data': [
'security/ir.model.access.csv',
'views/batch_payslip_view.xml',
],
'assets': {
'web.assets_backend': [
# Internal module JS and XML files (ensure correct paths within 'static/src')
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js',
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
],
},
'installable': True,
}

View File

@ -1,312 +1,312 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from collections import defaultdict
from functools import reduce
class HrPayslipRun(models.Model):
_inherit = 'hr.payslip.run'
@api.model
def get_consolidated_attendance_data(self, payslip_run_id):
"""
Returns consolidated attendance and leave data for all employees in the payslip run
"""
# Get all payslips in this batch
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
if not payslips:
return []
result = []
for slip in payslips:
employee = slip.employee_id
contract = slip.contract_id
# Get attendance data
attendance_days = self._get_attendance_days(slip)
worked_days = self._get_worked_days(slip)
leave_days = self._get_leave_days(slip)
lop_days = self._get_lop_days(slip)
# Get leave balances
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
leave_taken = self._get_leave_taken(slip)
result.append({
'id': slip.id,
'employee_id': (employee.id, employee.name),
'employee_code': employee.employee_id or '',
'department_id': (employee.department_id.id,
employee.department_id.name) if employee.department_id else False,
'total_days': (slip.date_to - slip.date_from).days + 1,
'worked_days': worked_days,
'attendance_days': attendance_days,
'leave_days': leave_days,
'lop_days': lop_days,
'doj':contract.date_start,
'birthday':employee.birthday,
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
'sick_leave_taken': leave_taken.get('sick', 0),
'casual_leave_taken': leave_taken.get('casual', 0),
'privilege_leave_taken': leave_taken.get('privilege', 0),
'state': slip.state,
'lines':self.get_payslip_lines_data(slip),
})
return result
@api.model
def sub_columns(self,payslip_run_id):
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
names = payslips.line_ids.filtered(lambda x:x.amount != 0)
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
columns = []
for code, name in code_name_dict.items():
columns.append({
'title': name,
'dataIndx': code,
'width': 150,
'editable': False,
'summary': {'type': "sum_"},
})
return columns
def save_consolidated_attendance_data(self, payslip_run_id, data):
"""
Saves the edited attendance and leave data from the grid
"""
self.ensure_one()
for item in data:
slip = self.env['hr.payslip'].browse(item['id'])
if slip.state != 'draft':
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
# Update LOP days
if 'lop_days' in item:
self._update_lop_days(slip, float(item['lop_days']))
# Update leave days taken
leave_updates = {}
if 'sick_leave_taken' in item:
leave_updates['sick'] = float(item['sick_leave_taken'])
if 'casual_leave_taken' in item:
leave_updates['casual'] = float(item['casual_leave_taken'])
if 'privilege_leave_taken' in item:
leave_updates['privilege'] = float(item['privilege_leave_taken'])
if leave_updates:
self._update_leave_taken(slip, leave_updates)
return True
def recalculate_lop_days(self, payslip_run_id):
"""
Recalculates LOP days for all payslips in the batch based on attendance
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
for slip in payslips:
attendance_days = self._get_attendance_days(slip)
expected_days = (slip.date_to - slip.date_from).days + 1
lop_days = expected_days - attendance_days
self._update_lop_days(slip, lop_days)
return True
def validate_all_attendance_data(self, payslip_run_id):
"""
Marks all payslips in the batch as validated
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
if not payslips:
raise UserError(_("No draft payslips found in this batch"))
payslips.write({'state': 'verify'})
return True
# Helper methods
def _get_attendance_days(self, payslip):
"""
Returns number of days employee was present (based on attendance records)
"""
attendance_records = self.env['hr.attendance'].search([
('employee_id', '=', payslip.employee_id.id),
('check_in', '>=', payslip.date_from),
('check_in', '<=', payslip.date_to)
])
# Group by day
unique_days = set()
for att in attendance_records:
unique_days.add(att.check_in.date())
return len(unique_days)
def _get_worked_days(self, payslip):
"""
Returns number of working days (excluding weekends and holidays)
"""
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
def get_payslip_lines_data(self, payslip_id):
list = []
for line in payslip_id.line_ids:
list.append({
'name': line.name,
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
'category_id': line.category_id.name if line.category_id else False,
'amount': line.amount,
'quantity': line.quantity,
'rate': line.rate
})
return list
def _get_leave_days(self, payslip):
"""
Returns total leave days taken in this period
"""
leave_lines = payslip.worked_days_line_ids.filtered(
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
)
return sum(leave_lines.mapped('number_of_days'))
def _get_lop_days(self, payslip):
"""
Returns LOP days from payslip
"""
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
return lop_line.number_of_days if lop_line else 0
def _get_leave_taken(self, payslip):
"""
Returns leave days taken in this payslip period
"""
leave_lines = payslip.worked_days_line_ids
return {
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
'casual': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE120').mapped('number_of_days')),
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
}
def _update_lop_days(self, payslip, days):
"""
Updates LOP days in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
if lop_line:
if days > 0:
lop_line.write({'number_of_days': days})
else:
lop_line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _('Loss of Pay'),
'code': 'LOP',
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
def _get_leave_balances(self, employee, date_from, date_to):
Leave = self.env['hr.leave']
Allocation = self.env['hr.leave.allocation']
leave_types = self.env['hr.leave.type'].search([])
balances = {}
for leave_type in leave_types:
# Approved allocations within or before payslip period
allocations = Allocation.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
])
allocated = sum(alloc.number_of_days for alloc in allocations)
# Approved leaves within the payslip period
leaves = Leave.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('request_date_to', '<=', str(date_to))
])
used = sum(leave.number_of_days for leave in leaves)
# Key: leave code or fallback to name
code = leave_type.work_entry_type_id.code
balances[code] = allocated - used
return balances
def _update_leave_taken(self, payslip, leave_data):
"""
Updates leave days taken in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
for leave_type, days in leave_data.items():
code = leave_type.upper()
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
if line:
if days > 0:
line.write({'number_of_days': days})
else:
line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _(leave_type.capitalize() + ' Leave'),
'code': code,
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def get_payslip_lines_data(self, payslip_id):
payslip = self.browse(payslip_id)
return [{
'name': line.name,
'code': line.code,
'category_id': (line.category_id.id, line.category_id.name),
'amount': line.amount,
'quantity': line.quantity,
'rate': line.rate
} for line in payslip.line_ids]
def action_open_payslips(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
action['views'] = [[False, "form"]]
action['res_id'] = self.id
action['target'] = 'new'
return action
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from collections import defaultdict
from functools import reduce
class HrPayslipRun(models.Model):
_inherit = 'hr.payslip.run'
@api.model
def get_consolidated_attendance_data(self, payslip_run_id):
"""
Returns consolidated attendance and leave data for all employees in the payslip run
"""
# Get all payslips in this batch
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
if not payslips:
return []
result = []
for slip in payslips:
employee = slip.employee_id
contract = slip.contract_id
# Get attendance data
attendance_days = self._get_attendance_days(slip)
worked_days = self._get_worked_days(slip)
leave_days = self._get_leave_days(slip)
lop_days = self._get_lop_days(slip)
# Get leave balances
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
leave_taken = self._get_leave_taken(slip)
result.append({
'id': slip.id,
'employee_id': (employee.id, employee.name),
'employee_code': employee.employee_id or '',
'department_id': (employee.department_id.id,
employee.department_id.name) if employee.department_id else False,
'total_days': (slip.date_to - slip.date_from).days + 1,
'worked_days': worked_days,
'attendance_days': attendance_days,
'leave_days': leave_days,
'lop_days': lop_days,
'doj':contract.date_start,
'birthday':employee.birthday,
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
'sick_leave_taken': leave_taken.get('sick', 0),
'casual_leave_taken': leave_taken.get('casual', 0),
'privilege_leave_taken': leave_taken.get('privilege', 0),
'state': slip.state,
'lines':self.get_payslip_lines_data(slip),
})
return result
@api.model
def sub_columns(self,payslip_run_id):
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
names = payslips.line_ids.filtered(lambda x:x.amount != 0)
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
columns = []
for code, name in code_name_dict.items():
columns.append({
'title': name,
'dataIndx': code,
'width': 150,
'editable': False,
'summary': {'type': "sum_"},
})
return columns
def save_consolidated_attendance_data(self, payslip_run_id, data):
"""
Saves the edited attendance and leave data from the grid
"""
self.ensure_one()
for item in data:
slip = self.env['hr.payslip'].browse(item['id'])
if slip.state != 'draft':
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
# Update LOP days
if 'lop_days' in item:
self._update_lop_days(slip, float(item['lop_days']))
# Update leave days taken
leave_updates = {}
if 'sick_leave_taken' in item:
leave_updates['sick'] = float(item['sick_leave_taken'])
if 'casual_leave_taken' in item:
leave_updates['casual'] = float(item['casual_leave_taken'])
if 'privilege_leave_taken' in item:
leave_updates['privilege'] = float(item['privilege_leave_taken'])
if leave_updates:
self._update_leave_taken(slip, leave_updates)
return True
def recalculate_lop_days(self, payslip_run_id):
"""
Recalculates LOP days for all payslips in the batch based on attendance
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
for slip in payslips:
attendance_days = self._get_attendance_days(slip)
expected_days = (slip.date_to - slip.date_from).days + 1
lop_days = expected_days - attendance_days
self._update_lop_days(slip, lop_days)
return True
def validate_all_attendance_data(self, payslip_run_id):
"""
Marks all payslips in the batch as validated
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
if not payslips:
raise UserError(_("No draft payslips found in this batch"))
payslips.write({'state': 'verify'})
return True
# Helper methods
def _get_attendance_days(self, payslip):
"""
Returns number of days employee was present (based on attendance records)
"""
attendance_records = self.env['hr.attendance'].search([
('employee_id', '=', payslip.employee_id.id),
('check_in', '>=', payslip.date_from),
('check_in', '<=', payslip.date_to)
])
# Group by day
unique_days = set()
for att in attendance_records:
unique_days.add(att.check_in.date())
return len(unique_days)
def _get_worked_days(self, payslip):
"""
Returns number of working days (excluding weekends and holidays)
"""
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
def get_payslip_lines_data(self, payslip_id):
list = []
for line in payslip_id.line_ids:
list.append({
'name': line.name,
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
'category_id': line.category_id.name if line.category_id else False,
'amount': line.amount,
'quantity': line.quantity,
'rate': line.rate
})
return list
def _get_leave_days(self, payslip):
"""
Returns total leave days taken in this period
"""
leave_lines = payslip.worked_days_line_ids.filtered(
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
)
return sum(leave_lines.mapped('number_of_days'))
def _get_lop_days(self, payslip):
"""
Returns LOP days from payslip
"""
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
return lop_line.number_of_days if lop_line else 0
def _get_leave_taken(self, payslip):
"""
Returns leave days taken in this payslip period
"""
leave_lines = payslip.worked_days_line_ids
return {
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
'casual': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE120').mapped('number_of_days')),
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
}
def _update_lop_days(self, payslip, days):
"""
Updates LOP days in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
if lop_line:
if days > 0:
lop_line.write({'number_of_days': days})
else:
lop_line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _('Loss of Pay'),
'code': 'LOP',
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
def _get_leave_balances(self, employee, date_from, date_to):
Leave = self.env['hr.leave']
Allocation = self.env['hr.leave.allocation']
leave_types = self.env['hr.leave.type'].search([])
balances = {}
for leave_type in leave_types:
# Approved allocations within or before payslip period
allocations = Allocation.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
])
allocated = sum(alloc.number_of_days for alloc in allocations)
# Approved leaves within the payslip period
leaves = Leave.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('request_date_to', '<=', str(date_to))
])
used = sum(leave.number_of_days for leave in leaves)
# Key: leave code or fallback to name
code = leave_type.work_entry_type_id.code
balances[code] = allocated - used
return balances
def _update_leave_taken(self, payslip, leave_data):
"""
Updates leave days taken in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
for leave_type, days in leave_data.items():
code = leave_type.upper()
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
if line:
if days > 0:
line.write({'number_of_days': days})
else:
line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _(leave_type.capitalize() + ' Leave'),
'code': code,
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def get_payslip_lines_data(self, payslip_id):
payslip = self.browse(payslip_id)
return [{
'name': line.name,
'code': line.code,
'category_id': (line.category_id.id, line.category_id.name),
'amount': line.amount,
'quantity': line.quantity,
'rate': line.rate
} for line in payslip.line_ids]
def action_open_payslips(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
action['views'] = [[False, "form"]]
action['res_id'] = self.id
action['target'] = 'new'
return action

View File

@ -1 +1 @@
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink

1 id name model_id:id group_id perm_read perm_write perm_create perm_unlink

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="ConsolidatedPayslipGrid" owl="1">
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
</t>
</templates>
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="ConsolidatedPayslipGrid" owl="1">
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
</t>
</templates>

View File

@ -1,18 +1,18 @@
<odoo>
<record id="view_hr_payslip_run_form_inherit" model="ir.ui.view">
<field name="name">hr.payslip.run.form.inherit.consolidated.pqgrid.owl</field>
<field name="model">hr.payslip.run</field>
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page string="Consolidated Payslip">
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
</div>
</page>
</notebook>
</xpath>
</field>
</record>
</odoo>
<odoo>
<record id="view_hr_payslip_run_form_inherit" model="ir.ui.view">
<field name="name">hr.payslip.run.form.inherit.consolidated.pqgrid.owl</field>
<field name="model">hr.payslip.run</field>
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page string="Consolidated Payslip">
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
</div>
</page>
</notebook>
</xpath>
</field>
</record>
</odoo>

View File

@ -1,22 +1,22 @@
{
'name': 'CWF Timesheet Update',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Manage and update weekly timesheets for CWF department',
'author': 'Your Name or Company',
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
'data': [
# 'views/timesheet_form.xml',
'security/security.xml',
'security/ir.model.access.csv',
'views/timesheet_view.xml',
'views/timesheet_weekly_view.xml',
'data/email_template.xml',
],
'assets': {
'web.assets_backend': [
'cwf_timesheet/static/src/js/timesheet_form.js',
],
},
'application': True,
}
{
'name': 'CWF Timesheet Update',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Manage and update weekly timesheets for CWF department',
'author': 'Your Name or Company',
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
'data': [
# 'views/timesheet_form.xml',
'security/security.xml',
'security/ir.model.access.csv',
'views/timesheet_view.xml',
'views/timesheet_weekly_view.xml',
'data/email_template.xml',
],
'assets': {
'web.assets_backend': [
'cwf_timesheet/static/src/js/timesheet_form.js',
],
},
'application': True,
}

View File

@ -1,9 +1,9 @@
from odoo import http
from odoo.http import request
class TimesheetController(http.Controller):
@http.route('/timesheet/form', auth='user', website=True)
def timesheet_form(self, **kw):
# This will render the template for the timesheet form
return request.render('timesheet_form', {})
from odoo import http
from odoo.http import request
class TimesheetController(http.Controller):
@http.route('/timesheet/form', auth='user', website=True)
def timesheet_form(self, **kw):
# This will render the template for the timesheet form
return request.render('timesheet_form', {})

View File

@ -1,45 +1,45 @@
<odoo>
<data noupdate="0">
<record id="email_template_timesheet_weekly_update" model="mail.template">
<field name="name">Timesheet Update Reminder</field>
<field name="model_id" ref="cwf_timesheet.model_cwf_weekly_timesheet"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ object.employee_id.user_id.email }}</field>
<field name="subject">Reminder: Update Your Weekly Timesheet</field>
<field name="description">
Reminder to employee to update their weekly timesheet.
</field>
<field name="body_html" type="html">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['employee_name']">Employee</t>,
<br/>
<br/>
I hope this message finds you in good spirits. I would like to remind you to please update your weekly timesheet for the period from
<strong>
<t t-esc="ctx['week_from']"/>
</strong>
to
<strong>
<t t-esc="ctx['week_to']"/>
</strong>.
Timely updates are crucial for maintaining accurate records and ensuring smooth processing.
<br/>
<br/>
To make things easier, you can use the link below to update your timesheet:
<br/>
<a href="https://ftprotech.in/odoo/action-261" class="cta-button" target="_blank">Update Timesheet</a>
<br/>
<br/>
Thank you for your attention.
<br/>
Best regards,
<br/>
<strong>Fast Track Project Pvt Ltd.</strong>
<br/>
<br/>
<a href="https://ftprotech.in/" target="_blank">Visit our site</a> for more information.
</p>
</field>
</record>
</data>
</odoo>
<odoo>
<data noupdate="0">
<record id="email_template_timesheet_weekly_update" model="mail.template">
<field name="name">Timesheet Update Reminder</field>
<field name="model_id" ref="cwf_timesheet.model_cwf_weekly_timesheet"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ object.employee_id.user_id.email }}</field>
<field name="subject">Reminder: Update Your Weekly Timesheet</field>
<field name="description">
Reminder to employee to update their weekly timesheet.
</field>
<field name="body_html" type="html">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['employee_name']">Employee</t>,
<br/>
<br/>
I hope this message finds you in good spirits. I would like to remind you to please update your weekly timesheet for the period from
<strong>
<t t-esc="ctx['week_from']"/>
</strong>
to
<strong>
<t t-esc="ctx['week_to']"/>
</strong>.
Timely updates are crucial for maintaining accurate records and ensuring smooth processing.
<br/>
<br/>
To make things easier, you can use the link below to update your timesheet:
<br/>
<a href="https://ftprotech.in/odoo/action-261" class="cta-button" target="_blank">Update Timesheet</a>
<br/>
<br/>
Thank you for your attention.
<br/>
Best regards,
<br/>
<strong>Fast Track Project Pvt Ltd.</strong>
<br/>
<br/>
<a href="https://ftprotech.in/" target="_blank">Visit our site</a> for more information.
</p>
</field>
</record>
</data>
</odoo>

View File

@ -1,399 +1,399 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from datetime import datetime, timedelta
import datetime as dt
from odoo import _
from calendar import month_name, month
from datetime import date
class CwfTimesheetYearly(models.Model):
_name = 'cwf.timesheet.calendar'
_description = "CWF Timesheet Calendar"
_rec_name = 'name'
name = fields.Char(string='Year Name', required=True)
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
_sql_constraints = [
('unique_year', 'unique(name)', 'The year must be unique!')
]
@api.constrains('name')
def _check_year_format(self):
for record in self:
if not record.name.isdigit() or len(record.name) != 4:
raise ValidationError("Year Name must be a 4-digit number.")
def generate_week_period(self):
for record in self:
record.week_period.unlink()
year = int(record.name)
# Find the first Monday of the year
start_date = datetime(year, 1, 1)
while start_date.weekday() != 0: # Monday is 0 in weekday()
start_date += timedelta(days=1)
# Generate weeks from Monday to Sunday
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
end_date = start_date + timedelta(days=6)
self.env['cwf.timesheet'].create({
'name': f'Week {start_date.strftime("%W")}, {year}',
'week_start_date': start_date.date(),
'week_end_date': end_date.date(),
'cwf_calendar_id': record.id,
})
start_date += timedelta(days=7)
def action_generate_weeks(self):
self.generate_week_period()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
class CwfTimesheet(models.Model):
_name = 'cwf.timesheet'
_description = 'CWF Weekly Timesheet'
_rec_name = 'name'
name = fields.Char(string='Week Name', required=True)
week_start_date = fields.Date(string='Week Start Date', required=True)
week_end_date = fields.Date(string='Week End Date', required=True)
status = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted')
], default='draft', string='Status')
lines = fields.One2many('cwf.timesheet.line','week_id')
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
start_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Start Month',
compute='_compute_months',
store=True
)
end_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='End Month',
compute='_compute_months',
store=True
)
@api.depends('week_start_date', 'week_end_date')
def _compute_months(self):
for rec in self:
if rec.week_start_date:
rec.start_month = str(rec.week_start_date.month)
else:
rec.start_month = False
if rec.week_end_date:
rec.end_month = str(rec.week_end_date.month)
else:
rec.end_month = False
@api.depends('name','week_start_date','week_end_date')
def _compute_display_name(self):
for rec in self:
rec.display_name = rec.name if not rec.week_start_date and rec.week_end_date else "%s (%s - %s)"%(rec.name,rec.week_start_date.strftime('%-d %b'), rec.week_end_date.strftime('%-d %b') )
def send_timesheet_update_email(self):
template = self.env.ref('cwf_timesheet.email_template_timesheet_weekly_update')
# Ensure that we have a valid employee email
current_date = fields.Date.from_string(self.week_start_date)
end_date = fields.Date.from_string(self.week_end_date)
if current_date > end_date:
raise UserError('The start date cannot be after the end date.')
# Get all employees in the department
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
print(employees)
# Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date:
for employee in employees:
existing_record = self.env['cwf.weekly.timesheet'].sudo().search([
('week_id', '=', self.id),
('employee_id', '=', employee.id)
], limit=1)
if not existing_record:
self.env['cwf.timesheet.line'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'week_day':current_date,
})
current_date += timedelta(days=1)
self.status = 'submitted'
for employee in employees:
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
if not weekly_timesheet_exists:
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'status': 'draft'
})
else:
weekly_timesheet = weekly_timesheet_exists
# Generate the URL for the newly created weekly_timesheet
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
weekly_timesheet.update_attendance()
if employee.work_email and weekly_timesheet:
email_values = {
'email_to': employee.work_email, # Email body from template
'subject': 'Timesheet Update Notification',
}
body_html = template.body_html.replace(
'https://ftprotech.in/odoo/action-261',
record_url
),
render_ctx = {'employee_name':weekly_timesheet.employee_id.name,'week_from':weekly_timesheet.week_id.week_start_date,'week_to':weekly_timesheet.week_id.week_end_date}
template.with_context(default_body_html=body_html,**render_ctx).send_mail(weekly_timesheet.id, email_values=email_values, force_send=True)
class CwfWeeklyTimesheet(models.Model):
_name = "cwf.weekly.timesheet"
_description = "CWF Weekly Timesheet"
_rec_name = 'employee_id'
def _default_week_id(self):
current_date = fields.Date.today()
timesheet = self.env['cwf.timesheet'].sudo().search([
('week_start_date', '<=', current_date),
('week_end_date', '>=', current_date)
], limit=1)
return timesheet.id if timesheet else False
def _get_week_id_domain(self):
for rec in self:
return [('week_start_date.month_number','=',2)]
pass
month_id = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Month'
)
week_id = fields.Many2one('cwf.timesheet', 'Week', default=lambda self: self._default_week_id())
employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id.id)
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
week_start_date = fields.Date(related='week_id.week_start_date')
week_end_date = fields.Date(related='week_id.week_end_date')
@api.onchange('month_id')
def _onchange_month(self):
if self.month_id:
year = self.week_start_date.year if self.week_start_date else fields.Date.today().year
start = date(year, int(self.month_id), 1)
if int(self.month_id) == 12:
end = date(year + 1, 1, 1) - timedelta(days=1)
else:
end = date(year, int(self.month_id) + 1, 1) - timedelta(days=1)
self = self.with_context(month_start=start, month_end=end)
@api.onchange('week_id')
def _onchange_week_id(self):
if self.week_id and self.week_id.week_start_date:
self.month_id = str(self.week_id.week_start_date.month)
@api.constrains('week_id', 'employee_id')
def _check_unique_week_employee(self):
for record in self:
if record.week_id.week_start_date > fields.Date.today():
raise ValidationError(_("You Can't select future week period"))
# Search for existing records with the same week_id and employee_id
existing_record = self.env['cwf.weekly.timesheet'].search([
('week_id', '=', record.week_id.id),
('employee_id', '=', record.employee_id.id)
], limit=1)
# If an existing record is found and it's not the current record (in case of update), raise an error
if existing_record and existing_record.id != record.id:
raise ValidationError("A timesheet for this employee already exists for the selected week.")
def update_attendance(self):
for rec in self:
# Get the week start and end date
week_start_date = rec.week_id.week_start_date
week_end_date = rec.week_id.week_end_date
# Convert start and end dates to datetime objects for proper filtering
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
# Delete timesheet lines that are outside the week range
rec.cwf_timesheet_lines.filtered(lambda line:
line.week_day < week_start_date or line.week_day > week_end_date
).unlink()
# Search for attendance records that fall within the week period and match the employee
hr_attendance_records = self.env['hr.attendance'].sudo().search([
('check_in', '>=', week_start_datetime),
('check_out', '<=', week_end_datetime),
('employee_id', '=', rec.employee_id.id)
])
# Group the attendance records by date
attendance_by_date = {}
for attendance in hr_attendance_records:
attendance_date = attendance.check_in.date()
if attendance_date not in attendance_by_date:
attendance_by_date[attendance_date] = []
attendance_by_date[attendance_date].append(attendance)
# Get all the dates within the week period
all_week_dates = [week_start_date + timedelta(days=i) for i in
range((week_end_date - week_start_date).days + 1)]
# Create or update timesheet lines for each day in the week
for date in all_week_dates:
# Check if there is attendance for this date
if date in attendance_by_date:
# If there are multiple attendance records, take the earliest check_in and latest check_out
earliest_check_in = min(attendance.check_in for attendance in attendance_by_date[date])
latest_check_out = max(attendance.check_out for attendance in attendance_by_date[date])
if (earliest_check_in + timedelta(hours=5, minutes=30)).date() > date:
earliest_check_in = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
if (latest_check_out + timedelta(hours=5, minutes=30)).date() > date:
latest_check_out = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
# Check if a timesheet line for this employee, week, and date already exists
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if existing_timesheet_line:
# If it exists, update the existing record
existing_timesheet_line.write({
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If it doesn't exist, create a new timesheet line with present state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If no attendance exists for this date, create a new timesheet line with time_off state_type
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if not existing_timesheet_line:
if date.weekday() != 5 and date.weekday() != 6:
# If no record exists for this date, create a new timesheet line with time_off state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'state_type': 'time_off',
})
def action_submit(self):
for rec in self:
for timesheet in rec.cwf_timesheet_lines:
timesheet.action_submit()
rec.status = 'submitted'
class CwfTimesheetLine(models.Model):
_name = 'cwf.timesheet.line'
_description = 'CWF Weekly Timesheet Lines'
_rec_name = 'employee_id'
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
week_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
week_day = fields.Date(string='Date')
check_in_date = fields.Datetime(string='Checkin')
check_out_date = fields.Datetime(string='Checkout ')
is_updated = fields.Boolean('Attendance Updated')
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('half_day','Half Day'),('present','Present')], default='draft', required=True)
@api.constrains('week_day', 'check_in_date', 'check_out_date')
def _check_week_day_and_times(self):
for record in self:
# Ensure week_day is within the week range
if record.week_id:
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
raise ValidationError(
"The selected 'week_day' must be within the range of the week from %s to %s." %
(record.week_id.week_start_date, record.week_id.week_end_date)
)
# Ensure check_in_date and check_out_date are on the selected week_day
if record.check_in_date:
if record.check_in_date.date() != record.week_day:
raise ValidationError(
"The 'check_in_date' must be on the selected Date."
)
if record.check_out_date:
if record.check_out_date.date() != record.week_day:
raise ValidationError(
"The 'check_out_date' must be on the selected Date."
)
def action_submit(self):
if self.state_type == 'draft' or not self.state_type:
raise ValidationError(_('State type should not Draft or Empty'))
if self.state_type not in ['holiday','time_off'] and not (self.check_in_date or self.check_out_date):
raise ValidationError(_('Please enter Check details'))
self._update_attendance()
def _update_attendance(self):
attendance_obj = self.env['hr.attendance']
for record in self:
if record.check_in_date != False and record.check_out_date != False and record.employee_id:
first_check_in = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_in")
last_check_out = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_out desc")
if first_check_in or last_check_out:
if first_check_in.sudo().check_in != record.check_in_date:
first_check_in.sudo().check_in = record.check_in_date
if last_check_out.sudo().check_out != record.check_out_date:
last_check_out.sudo().check_out = record.check_out_date
else:
attendance_obj.sudo().create({
'employee_id': record.employee_id.id,
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
})
record.is_updated = True
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from datetime import datetime, timedelta
import datetime as dt
from odoo import _
from calendar import month_name, month
from datetime import date
class CwfTimesheetYearly(models.Model):
_name = 'cwf.timesheet.calendar'
_description = "CWF Timesheet Calendar"
_rec_name = 'name'
name = fields.Char(string='Year Name', required=True)
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
_sql_constraints = [
('unique_year', 'unique(name)', 'The year must be unique!')
]
@api.constrains('name')
def _check_year_format(self):
for record in self:
if not record.name.isdigit() or len(record.name) != 4:
raise ValidationError("Year Name must be a 4-digit number.")
def generate_week_period(self):
for record in self:
record.week_period.unlink()
year = int(record.name)
# Find the first Monday of the year
start_date = datetime(year, 1, 1)
while start_date.weekday() != 0: # Monday is 0 in weekday()
start_date += timedelta(days=1)
# Generate weeks from Monday to Sunday
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
end_date = start_date + timedelta(days=6)
self.env['cwf.timesheet'].create({
'name': f'Week {start_date.strftime("%W")}, {year}',
'week_start_date': start_date.date(),
'week_end_date': end_date.date(),
'cwf_calendar_id': record.id,
})
start_date += timedelta(days=7)
def action_generate_weeks(self):
self.generate_week_period()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
class CwfTimesheet(models.Model):
_name = 'cwf.timesheet'
_description = 'CWF Weekly Timesheet'
_rec_name = 'name'
name = fields.Char(string='Week Name', required=True)
week_start_date = fields.Date(string='Week Start Date', required=True)
week_end_date = fields.Date(string='Week End Date', required=True)
status = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted')
], default='draft', string='Status')
lines = fields.One2many('cwf.timesheet.line','week_id')
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
start_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Start Month',
compute='_compute_months',
store=True
)
end_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='End Month',
compute='_compute_months',
store=True
)
@api.depends('week_start_date', 'week_end_date')
def _compute_months(self):
for rec in self:
if rec.week_start_date:
rec.start_month = str(rec.week_start_date.month)
else:
rec.start_month = False
if rec.week_end_date:
rec.end_month = str(rec.week_end_date.month)
else:
rec.end_month = False
@api.depends('name','week_start_date','week_end_date')
def _compute_display_name(self):
for rec in self:
rec.display_name = rec.name if not rec.week_start_date and rec.week_end_date else "%s (%s - %s)"%(rec.name,rec.week_start_date.strftime('%-d %b'), rec.week_end_date.strftime('%-d %b') )
def send_timesheet_update_email(self):
template = self.env.ref('cwf_timesheet.email_template_timesheet_weekly_update')
# Ensure that we have a valid employee email
current_date = fields.Date.from_string(self.week_start_date)
end_date = fields.Date.from_string(self.week_end_date)
if current_date > end_date:
raise UserError('The start date cannot be after the end date.')
# Get all employees in the department
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
print(employees)
# Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date:
for employee in employees:
existing_record = self.env['cwf.weekly.timesheet'].sudo().search([
('week_id', '=', self.id),
('employee_id', '=', employee.id)
], limit=1)
if not existing_record:
self.env['cwf.timesheet.line'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'week_day':current_date,
})
current_date += timedelta(days=1)
self.status = 'submitted'
for employee in employees:
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
if not weekly_timesheet_exists:
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'status': 'draft'
})
else:
weekly_timesheet = weekly_timesheet_exists
# Generate the URL for the newly created weekly_timesheet
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
weekly_timesheet.update_attendance()
if employee.work_email and weekly_timesheet:
email_values = {
'email_to': employee.work_email, # Email body from template
'subject': 'Timesheet Update Notification',
}
body_html = template.body_html.replace(
'https://ftprotech.in/odoo/action-261',
record_url
),
render_ctx = {'employee_name':weekly_timesheet.employee_id.name,'week_from':weekly_timesheet.week_id.week_start_date,'week_to':weekly_timesheet.week_id.week_end_date}
template.with_context(default_body_html=body_html,**render_ctx).send_mail(weekly_timesheet.id, email_values=email_values, force_send=True)
class CwfWeeklyTimesheet(models.Model):
_name = "cwf.weekly.timesheet"
_description = "CWF Weekly Timesheet"
_rec_name = 'employee_id'
def _default_week_id(self):
current_date = fields.Date.today()
timesheet = self.env['cwf.timesheet'].sudo().search([
('week_start_date', '<=', current_date),
('week_end_date', '>=', current_date)
], limit=1)
return timesheet.id if timesheet else False
def _get_week_id_domain(self):
for rec in self:
return [('week_start_date.month_number','=',2)]
pass
month_id = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Month'
)
week_id = fields.Many2one('cwf.timesheet', 'Week', default=lambda self: self._default_week_id())
employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id.id)
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
week_start_date = fields.Date(related='week_id.week_start_date')
week_end_date = fields.Date(related='week_id.week_end_date')
@api.onchange('month_id')
def _onchange_month(self):
if self.month_id:
year = self.week_start_date.year if self.week_start_date else fields.Date.today().year
start = date(year, int(self.month_id), 1)
if int(self.month_id) == 12:
end = date(year + 1, 1, 1) - timedelta(days=1)
else:
end = date(year, int(self.month_id) + 1, 1) - timedelta(days=1)
self = self.with_context(month_start=start, month_end=end)
@api.onchange('week_id')
def _onchange_week_id(self):
if self.week_id and self.week_id.week_start_date:
self.month_id = str(self.week_id.week_start_date.month)
@api.constrains('week_id', 'employee_id')
def _check_unique_week_employee(self):
for record in self:
if record.week_id.week_start_date > fields.Date.today():
raise ValidationError(_("You Can't select future week period"))
# Search for existing records with the same week_id and employee_id
existing_record = self.env['cwf.weekly.timesheet'].search([
('week_id', '=', record.week_id.id),
('employee_id', '=', record.employee_id.id)
], limit=1)
# If an existing record is found and it's not the current record (in case of update), raise an error
if existing_record and existing_record.id != record.id:
raise ValidationError("A timesheet for this employee already exists for the selected week.")
def update_attendance(self):
for rec in self:
# Get the week start and end date
week_start_date = rec.week_id.week_start_date
week_end_date = rec.week_id.week_end_date
# Convert start and end dates to datetime objects for proper filtering
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
# Delete timesheet lines that are outside the week range
rec.cwf_timesheet_lines.filtered(lambda line:
line.week_day < week_start_date or line.week_day > week_end_date
).unlink()
# Search for attendance records that fall within the week period and match the employee
hr_attendance_records = self.env['hr.attendance'].sudo().search([
('check_in', '>=', week_start_datetime),
('check_out', '<=', week_end_datetime),
('employee_id', '=', rec.employee_id.id)
])
# Group the attendance records by date
attendance_by_date = {}
for attendance in hr_attendance_records:
attendance_date = attendance.check_in.date()
if attendance_date not in attendance_by_date:
attendance_by_date[attendance_date] = []
attendance_by_date[attendance_date].append(attendance)
# Get all the dates within the week period
all_week_dates = [week_start_date + timedelta(days=i) for i in
range((week_end_date - week_start_date).days + 1)]
# Create or update timesheet lines for each day in the week
for date in all_week_dates:
# Check if there is attendance for this date
if date in attendance_by_date:
# If there are multiple attendance records, take the earliest check_in and latest check_out
earliest_check_in = min(attendance.check_in for attendance in attendance_by_date[date])
latest_check_out = max(attendance.check_out for attendance in attendance_by_date[date])
if (earliest_check_in + timedelta(hours=5, minutes=30)).date() > date:
earliest_check_in = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
if (latest_check_out + timedelta(hours=5, minutes=30)).date() > date:
latest_check_out = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
# Check if a timesheet line for this employee, week, and date already exists
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if existing_timesheet_line:
# If it exists, update the existing record
existing_timesheet_line.write({
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If it doesn't exist, create a new timesheet line with present state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If no attendance exists for this date, create a new timesheet line with time_off state_type
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if not existing_timesheet_line:
if date.weekday() != 5 and date.weekday() != 6:
# If no record exists for this date, create a new timesheet line with time_off state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'state_type': 'time_off',
})
def action_submit(self):
for rec in self:
for timesheet in rec.cwf_timesheet_lines:
timesheet.action_submit()
rec.status = 'submitted'
class CwfTimesheetLine(models.Model):
_name = 'cwf.timesheet.line'
_description = 'CWF Weekly Timesheet Lines'
_rec_name = 'employee_id'
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
week_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
week_day = fields.Date(string='Date')
check_in_date = fields.Datetime(string='Checkin')
check_out_date = fields.Datetime(string='Checkout ')
is_updated = fields.Boolean('Attendance Updated')
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('half_day','Half Day'),('present','Present')], default='draft', required=True)
@api.constrains('week_day', 'check_in_date', 'check_out_date')
def _check_week_day_and_times(self):
for record in self:
# Ensure week_day is within the week range
if record.week_id:
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
raise ValidationError(
"The selected 'week_day' must be within the range of the week from %s to %s." %
(record.week_id.week_start_date, record.week_id.week_end_date)
)
# Ensure check_in_date and check_out_date are on the selected week_day
if record.check_in_date:
if record.check_in_date.date() != record.week_day:
raise ValidationError(
"The 'check_in_date' must be on the selected Date."
)
if record.check_out_date:
if record.check_out_date.date() != record.week_day:
raise ValidationError(
"The 'check_out_date' must be on the selected Date."
)
def action_submit(self):
if self.state_type == 'draft' or not self.state_type:
raise ValidationError(_('State type should not Draft or Empty'))
if self.state_type not in ['holiday','time_off'] and not (self.check_in_date or self.check_out_date):
raise ValidationError(_('Please enter Check details'))
self._update_attendance()
def _update_attendance(self):
attendance_obj = self.env['hr.attendance']
for record in self:
if record.check_in_date != False and record.check_out_date != False and record.employee_id:
first_check_in = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_in")
last_check_out = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_out desc")
if first_check_in or last_check_out:
if first_check_in.sudo().check_in != record.check_in_date:
first_check_in.sudo().check_in = record.check_in_date
if last_check_out.sudo().check_out != record.check_out_date:
last_check_out.sudo().check_out = record.check_out_date
else:
attendance_obj.sudo().create({
'employee_id': record.employee_id.id,
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
})
record.is_updated = True

View File

@ -1,15 +1,15 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,base.group_user,1,0,0,0
access_cwf_timesheet_manager,access.cwf.timesheet,model_cwf_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar,cwf_timesheet_calendar,model_cwf_timesheet_calendar,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar_user,cwf_timesheet_calendar_user,model_cwf_timesheet_calendar,base.group_user,1,0,0,0
access_cwf_timesheet_line_manager,access.cwf.timesheet.line.manager,model_cwf_timesheet_line,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,hr_employee_extended.group_external_user,1,1,1,1
access_cwf_weekly_timesheet_manager,cwf.weekly.timesheet.manager access,model_cwf_weekly_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_weekly_timesheet_user,cwf.weekly.timesheet access,model_cwf_weekly_timesheet,hr_employee_extended.group_external_user,1,1,1,0
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,base.group_user,1,0,0,0
access_cwf_timesheet_manager,access.cwf.timesheet,model_cwf_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar,cwf_timesheet_calendar,model_cwf_timesheet_calendar,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar_user,cwf_timesheet_calendar_user,model_cwf_timesheet_calendar,base.group_user,1,0,0,0
access_cwf_timesheet_line_manager,access.cwf.timesheet.line.manager,model_cwf_timesheet_line,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,hr_employee_extended.group_external_user,1,1,1,1
access_cwf_weekly_timesheet_manager,cwf.weekly.timesheet.manager access,model_cwf_weekly_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_weekly_timesheet_user,cwf.weekly.timesheet access,model_cwf_weekly_timesheet,hr_employee_extended.group_external_user,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_cwf_timesheet_user access.cwf.timesheet model_cwf_timesheet base.group_user 1 0 0 0
3 access_cwf_timesheet_manager access.cwf.timesheet model_cwf_timesheet hr_attendance.group_hr_attendance_manager 1 1 1 1
4 access_cwf_timesheet_calendar cwf_timesheet_calendar model_cwf_timesheet_calendar hr_attendance.group_hr_attendance_manager 1 1 1 1
5 access_cwf_timesheet_calendar_user cwf_timesheet_calendar_user model_cwf_timesheet_calendar base.group_user 1 0 0 0
6 access_cwf_timesheet_line_manager access.cwf.timesheet.line.manager model_cwf_timesheet_line hr_attendance.group_hr_attendance_manager 1 1 1 1
7 access_cwf_timesheet_line_user access.cwf.timesheet.line model_cwf_timesheet_line hr_employee_extended.group_external_user 1 1 1 1
8 access_cwf_weekly_timesheet_manager cwf.weekly.timesheet.manager access model_cwf_weekly_timesheet hr_attendance.group_hr_attendance_manager 1 1 1 1
9 access_cwf_weekly_timesheet_user cwf.weekly.timesheet access model_cwf_weekly_timesheet hr_employee_extended.group_external_user 1 1 1 0
10
11
12
13
14
15

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="0">
<record id="cwf_weekly_timesheet_user_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet User Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_timesheet_line_user_rule" model="ir.rule">
<field name="name">CWF Timesheet Line User Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_weekly_timesheet_manager_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet manager Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
<record id="cwf_timesheet_line_manager_rule" model="ir.rule">
<field name="name">CWF Timesheet Line manager Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
</data>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="0">
<record id="cwf_weekly_timesheet_user_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet User Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_timesheet_line_user_rule" model="ir.rule">
<field name="name">CWF Timesheet Line User Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_weekly_timesheet_manager_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet manager Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
<record id="cwf_timesheet_line_manager_rule" model="ir.rule">
<field name="name">CWF Timesheet Line manager Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
</data>
</odoo>

View File

@ -1,54 +1,54 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
import { user } from "@web/core/user";
// Apply patch to NetflixProfileContainer prototype
patch(NetflixProfileContainer.prototype, {
/**
* @override
*/
setup() {
// Call parent setup method
super.setup(...arguments);
// Log the department of the logged-in employee (check if data is available)
// if (this.state && this.state.login_employee) {
// console.log(this.state.login_employee['department_id']);
// } else {
// console.error('Employee or department data is unavailable.');
// }
},
/**
* Override the hr_timesheets method
*/
async hr_timesheets() {
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
// Check the department of the logged-in employee
console.log(isExternalUser);
console.log("is external user");
debugger;
if (isExternalUser && this.state.login_employee.department_id) {
console.log("hello external");
// If the department is 'CWF', perform the action to open the timesheets
this.action.doAction({
name: "Timesheets",
type: 'ir.actions.act_window',
res_model: 'cwf.timesheet.line', // Ensure this model exists
view_mode: 'list,form',
views: [[false, 'list'], [false, 'form']],
context: {
'search_default_week_id': true,
},
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
target: 'current',
});
} else {
// If not 'CWF', call the base functionality
return super.hr_timesheets();
}
},
});
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
import { user } from "@web/core/user";
// Apply patch to NetflixProfileContainer prototype
patch(NetflixProfileContainer.prototype, {
/**
* @override
*/
setup() {
// Call parent setup method
super.setup(...arguments);
// Log the department of the logged-in employee (check if data is available)
// if (this.state && this.state.login_employee) {
// console.log(this.state.login_employee['department_id']);
// } else {
// console.error('Employee or department data is unavailable.');
// }
},
/**
* Override the hr_timesheets method
*/
async hr_timesheets() {
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
// Check the department of the logged-in employee
console.log(isExternalUser);
console.log("is external user");
debugger;
if (isExternalUser && this.state.login_employee.department_id) {
console.log("hello external");
// If the department is 'CWF', perform the action to open the timesheets
this.action.doAction({
name: "Timesheets",
type: 'ir.actions.act_window',
res_model: 'cwf.timesheet.line', // Ensure this model exists
view_mode: 'list,form',
views: [[false, 'list'], [false, 'form']],
context: {
'search_default_week_id': true,
},
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
target: 'current',
});
} else {
// If not 'CWF', call the base functionality
return super.hr_timesheets();
}
},
});

View File

@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="timesheet_form">
<div class="container">
<h2>Weekly Timesheet</h2>
<form class="timesheet-form">
<div class="form-group">
<label for="employee">Employee</label>
<input t-att-value="state.employee" type="text" id="employee" class="form-control"/>
</div>
<div class="form-group">
<label for="weekStartDate">Week Start Date</label>
<input type="datetime-local" t-model="state.weekStartDate" id="weekStartDate" class="form-control"/>
</div>
<div class="form-group">
<label for="weekEndDate">Week End Date</label>
<input type="datetime-local" t-model="state.weekEndDate" id="weekEndDate" class="form-control"/>
</div>
<div class="form-group">
<label for="totalHours">Total Hours Worked</label>
<input type="number" t-model="state.totalHours" id="totalHours" class="form-control" min="0"/>
</div>
<button type="button" t-on-click="submitForm" class="btn btn-primary">Submit Timesheet</button>
</form>
</div>
</t>
</templates>
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="timesheet_form">
<div class="container">
<h2>Weekly Timesheet</h2>
<form class="timesheet-form">
<div class="form-group">
<label for="employee">Employee</label>
<input t-att-value="state.employee" type="text" id="employee" class="form-control"/>
</div>
<div class="form-group">
<label for="weekStartDate">Week Start Date</label>
<input type="datetime-local" t-model="state.weekStartDate" id="weekStartDate" class="form-control"/>
</div>
<div class="form-group">
<label for="weekEndDate">Week End Date</label>
<input type="datetime-local" t-model="state.weekEndDate" id="weekEndDate" class="form-control"/>
</div>
<div class="form-group">
<label for="totalHours">Total Hours Worked</label>
<input type="number" t-model="state.totalHours" id="totalHours" class="form-control" min="0"/>
</div>
<button type="button" t-on-click="submitForm" class="btn btn-primary">Submit Timesheet</button>
</form>
</div>
</t>
</templates>

View File

@ -1,104 +1,104 @@
<odoo>
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.form</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<form string="CWF Timesheet Calendar">
<sheet>
<group>
<field name="name"/>
<button name="action_generate_weeks" string="Generate Week Periods" type="object"
class="oe_highlight"/>
</group>
<notebook>
<page string="Weeks">
<field name="week_period" context="{'order': 'week_start_date asc'}">
<list editable="bottom" decoration-success="status == 'submitted'">
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
<field name="start_month" column_invisible="1"/>
<field name="end_month" column_invisible="1"/>
<field name="status"/>
<button name="send_timesheet_update_email" string="Send Update Email"
invisible="status == 'submitted'" type="object"
confirm="You can't revert this action. Please check twice before Submitting?"
class="oe_highlight"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.list</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
<field name="name">CWF Timesheet Calendar</field>
<field name="res_model">cwf.timesheet.calendar</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_cwf_attendance_attendance" name="CWF" parent="hr_attendance.menu_hr_attendance_root"
sequence="6" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_calendar_form" name="CWF Timesheet Calendar" parent="menu_cwf_attendance_attendance"
action="cwf_timesheet.action_cwf_timesheet_calendar" groups="hr_attendance.group_hr_attendance_manager"/>
<record id="view_timesheet_form" model="ir.ui.view">
<field name="name">cwf.timesheet.form</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<form string="Timesheet">
<header>
<button name="send_timesheet_update_email" string="Send Email" type="object"
invisible="status != 'draft'"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" class="oe_inline" readonly="status != 'draft'"/>
</h1>
</div>
<group>
<!-- Section for Employee and Date Range -->
<group>
<field name="week_start_date" readonly="status != 'draft'"/>
<field name="week_end_date" readonly="status != 'draft'"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_timesheet_list" model="ir.ui.view">
<field name="name">cwf.timesheet.list</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet" model="ir.actions.act_window">
<field name="name">CWF Timesheet</field>
<field name="res_model">cwf.timesheet</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
<odoo>
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.form</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<form string="CWF Timesheet Calendar">
<sheet>
<group>
<field name="name"/>
<button name="action_generate_weeks" string="Generate Week Periods" type="object"
class="oe_highlight"/>
</group>
<notebook>
<page string="Weeks">
<field name="week_period" context="{'order': 'week_start_date asc'}">
<list editable="bottom" decoration-success="status == 'submitted'">
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
<field name="start_month" column_invisible="1"/>
<field name="end_month" column_invisible="1"/>
<field name="status"/>
<button name="send_timesheet_update_email" string="Send Update Email"
invisible="status == 'submitted'" type="object"
confirm="You can't revert this action. Please check twice before Submitting?"
class="oe_highlight"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.list</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
<field name="name">CWF Timesheet Calendar</field>
<field name="res_model">cwf.timesheet.calendar</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_cwf_attendance_attendance" name="CWF" parent="hr_attendance.menu_hr_attendance_root"
sequence="6" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_calendar_form" name="CWF Timesheet Calendar" parent="menu_cwf_attendance_attendance"
action="cwf_timesheet.action_cwf_timesheet_calendar" groups="hr_attendance.group_hr_attendance_manager"/>
<record id="view_timesheet_form" model="ir.ui.view">
<field name="name">cwf.timesheet.form</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<form string="Timesheet">
<header>
<button name="send_timesheet_update_email" string="Send Email" type="object"
invisible="status != 'draft'"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" class="oe_inline" readonly="status != 'draft'"/>
</h1>
</div>
<group>
<!-- Section for Employee and Date Range -->
<group>
<field name="week_start_date" readonly="status != 'draft'"/>
<field name="week_end_date" readonly="status != 'draft'"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_timesheet_list" model="ir.ui.view">
<field name="name">cwf.timesheet.list</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet" model="ir.actions.act_window">
<field name="name">CWF Timesheet</field>
<field name="res_model">cwf.timesheet</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -1,158 +1,158 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.form</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<form string="CWF Weekly Timesheet">
<header>
<button name="update_attendance" string="Update"
type="object" class="oe_highlight" invisible="status != 'draft'"/>
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
class="oe_highlight" invisible="status != 'draft'"/>
<field name="status" readonly="1" widget="statusbar"/>
</header>
<sheet>
<group>
<field name="month_id"/>
<field name="week_id" readonly="0" domain="['|',('start_month','=',month_id),('end_month','=',month_id)]"/>
<field name="employee_id" readonly="0" groups="hr_attendance.group_hr_attendance_manager"/>
<field name="employee_id" readonly="1" groups="hr_employee_extended.group_external_user"/>
<label for="week_start_date" string="Dates"/>
<div class="o_row">
<field name="week_start_date" widget="daterange" options="{'end_date_field.month()': 'week_end_date'}"/>
<field name="week_end_date" invisible="1"/>
</div>
</group>
<notebook>
<page string="Timesheet Lines">
<field name="cwf_timesheet_lines">
<list editable="bottom">
<field name="employee_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<!-- <button name="action_submit" string="Submit" type="object"-->
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.list</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="week_id"/>
<field name="employee_id"/>
<field name="status"/>
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
<!-- class="oe_highlight"/>-->
</list>
</field>
</record>
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.search</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<search>
<!-- Search by Week ID -->
<field name="week_id"/>
<!-- Search by Employee -->
<field name="employee_id"/>
<!-- Search by Status -->
<field name="status"/>
<!-- Optional: Add custom filters if needed -->
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
<group expand="0" string="Group By">
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<separator/>
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
<field name="name">CWF Weekly Timesheet</field>
<field name="res_model">cwf.weekly.timesheet</field>
<field name="view_mode">list,form</field>
<field name="context">
{
"search_default_by_week_id": 1,
"search_default_by_employee_id": 2
}
</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
</record>
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
<field name="name">cwf.timesheet.line.list</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
<field name="employee_id" readonly="1" force_save="1"/>
<field name="week_day" readonly="1" force_save="1"/>
<field name="check_in_date" readonly="is_updated == True"/>
<field name="check_out_date" readonly="is_updated == True"/>
<field name="state_type" readonly="is_updated == True"/>
<!-- <button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary"-->
<!-- invisible="is_updated == True"/>-->
</list>
</field>
</record>
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
<field name="name">cwf.timesheet.line.search</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<search string="Timesheets">
<field name="employee_id"/>
<field name="week_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<group expand="0" string="Group By">
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<separator/>
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
<field name="name">CWF Timesheet Lines</field>
<field name="res_model">cwf.timesheet.line</field>
<field name="view_mode">list</field>
<field name="context">{"search_default_by_week_id": 1, "search_default_by_employee_id": 1}</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_timesheet_line_search"/>
</record>
<menuitem id="menu_timesheet_form" name="CWF Weekly Timesheet "
parent="menu_cwf_attendance_attendance" action="action_cwf_weekly_timesheet" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_form_line" name="CWF Timesheet Lines"
parent="menu_cwf_attendance_attendance" action="action_cwf_timesheet_line" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.form</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<form string="CWF Weekly Timesheet">
<header>
<button name="update_attendance" string="Update"
type="object" class="oe_highlight" invisible="status != 'draft'"/>
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
class="oe_highlight" invisible="status != 'draft'"/>
<field name="status" readonly="1" widget="statusbar"/>
</header>
<sheet>
<group>
<field name="month_id"/>
<field name="week_id" readonly="0" domain="['|',('start_month','=',month_id),('end_month','=',month_id)]"/>
<field name="employee_id" readonly="0" groups="hr_attendance.group_hr_attendance_manager"/>
<field name="employee_id" readonly="1" groups="hr_employee_extended.group_external_user"/>
<label for="week_start_date" string="Dates"/>
<div class="o_row">
<field name="week_start_date" widget="daterange" options="{'end_date_field.month()': 'week_end_date'}"/>
<field name="week_end_date" invisible="1"/>
</div>
</group>
<notebook>
<page string="Timesheet Lines">
<field name="cwf_timesheet_lines">
<list editable="bottom">
<field name="employee_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<!-- <button name="action_submit" string="Submit" type="object"-->
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.list</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="week_id"/>
<field name="employee_id"/>
<field name="status"/>
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
<!-- class="oe_highlight"/>-->
</list>
</field>
</record>
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.search</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<search>
<!-- Search by Week ID -->
<field name="week_id"/>
<!-- Search by Employee -->
<field name="employee_id"/>
<!-- Search by Status -->
<field name="status"/>
<!-- Optional: Add custom filters if needed -->
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
<group expand="0" string="Group By">
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<separator/>
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
<field name="name">CWF Weekly Timesheet</field>
<field name="res_model">cwf.weekly.timesheet</field>
<field name="view_mode">list,form</field>
<field name="context">
{
"search_default_by_week_id": 1,
"search_default_by_employee_id": 2
}
</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
</record>
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
<field name="name">cwf.timesheet.line.list</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
<field name="employee_id" readonly="1" force_save="1"/>
<field name="week_day" readonly="1" force_save="1"/>
<field name="check_in_date" readonly="is_updated == True"/>
<field name="check_out_date" readonly="is_updated == True"/>
<field name="state_type" readonly="is_updated == True"/>
<!-- <button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary"-->
<!-- invisible="is_updated == True"/>-->
</list>
</field>
</record>
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
<field name="name">cwf.timesheet.line.search</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<search string="Timesheets">
<field name="employee_id"/>
<field name="week_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<group expand="0" string="Group By">
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<separator/>
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
<field name="name">CWF Timesheet Lines</field>
<field name="res_model">cwf.timesheet.line</field>
<field name="view_mode">list</field>
<field name="context">{"search_default_by_week_id": 1, "search_default_by_employee_id": 1}</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_timesheet_line_search"/>
</record>
<menuitem id="menu_timesheet_form" name="CWF Weekly Timesheet "
parent="menu_cwf_attendance_attendance" action="action_cwf_weekly_timesheet" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_form_line" name="CWF Timesheet Lines"
parent="menu_cwf_attendance_attendance" action="action_cwf_timesheet_line" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
</odoo>

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,19 @@
{
"name": "Document Parser",
"summary": "Reusable AI-assisted document text and data extraction",
"version": "1.0.0",
"category": "Tools",
"author": "Pranay",
"website": "https://www.ftprotech.com",
"license": "LGPL-3",
"depends": ["base"],
"data": [
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,
"auto_install": False,
"external_dependencies": {
"python": ["requests"],
},
}

View File

@ -0,0 +1,2 @@
from . import document_parser_service
from . import res_config_settings

View File

@ -0,0 +1,444 @@
import base64
import json
import logging
import mimetypes
import re
from io import BytesIO
import requests
from odoo import _, api, models
from odoo.exceptions import UserError
try:
import pytesseract
except Exception: # pragma: no cover - optional dependency
pytesseract = None
try:
from PIL import Image
except Exception: # pragma: no cover - optional dependency
Image = None
try:
from pdf2image import convert_from_bytes
except Exception: # pragma: no cover - optional dependency
convert_from_bytes = None
try:
from pypdf import PdfReader
except Exception: # pragma: no cover - optional dependency
PdfReader = None
try:
from docx import Document
except Exception: # pragma: no cover - optional dependency
Document = None
_logger = logging.getLogger(__name__)
class DocumentParserService(models.AbstractModel):
_name = "document.parser.service"
_description = "Document Parser Service"
TOGETHER_ENDPOINT = "https://api.together.xyz/v1/chat/completions"
OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
TOGETHER_MODELS = [
"Qwen/Qwen2.5-7B-Instruct-Turbo",
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
]
OPENROUTER_MODELS = [
"qwen/qwen-2.5-7b-instruct",
"qwen/qwen-2.5-7b-instruct:free",
"deepseek/deepseek-chat:free",
]
@api.model
def parse_document(
self,
file_content,
filename=None,
required_fields=None,
extra_instructions=None,
json_schema=None,
):
if not file_content:
raise UserError(_("No document provided."))
if not filename:
raise UserError(_("Filename is required."))
binary = self._decode_file_content(file_content)
mimetype = self._detect_mimetype(binary, filename)
text_content = self._extract_text(binary, mimetype)
fields_spec = self._normalize_required_fields(required_fields or {})
if not text_content.strip():
return {
"filename": filename,
"mimetype": mimetype,
"text": "",
"result": {},
"provider": False,
"errors": [_("No text could be extracted from the document.")],
"error": _("No text could be extracted from the document."),
}
schema_text = json_schema or self._build_json_schema_text(fields_spec)
ai_result, provider_used, provider_errors = self._send_to_ai(
text_content=text_content[:45000],
schema_text=schema_text,
extra_instructions=extra_instructions,
)
if not ai_result:
ai_result = self._extract_with_heuristics(text_content, fields_spec)
ai_result = ai_result or {}
error_message = False
if not ai_result and provider_errors:
error_message = "; ".join(provider_errors[:3])
return {
"filename": filename,
"mimetype": mimetype,
"text": text_content,
"result": ai_result,
"provider": provider_used,
"errors": provider_errors,
"error": error_message,
}
@api.model
def extract_requested_data(self, file_content, filename, required_fields, extra_instructions=None, json_schema=None):
return self.parse_document(
file_content=file_content,
filename=filename,
required_fields=required_fields,
extra_instructions=extra_instructions,
json_schema=json_schema,
)["result"]
def _decode_file_content(self, file_content):
if isinstance(file_content, bytes):
if file_content.startswith((b"%PDF", b"\xFF\xD8", b"\x89PNG", b"PK")):
return file_content
try:
return base64.b64decode(file_content)
except Exception:
return file_content
if isinstance(file_content, str):
try:
return base64.b64decode(file_content)
except Exception as exc:
raise UserError(_("Invalid base64 document.")) from exc
raise UserError(_("Unsupported file format."))
def _detect_mimetype(self, binary, filename):
if filename:
guessed = mimetypes.guess_type(filename)[0]
if guessed:
return guessed
if binary.startswith(b"%PDF"):
return "application/pdf"
if binary.startswith(b"\xFF\xD8"):
return "image/jpeg"
if binary.startswith(b"\x89PNG"):
return "image/png"
if binary[:2] == b"PK":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
return "application/octet-stream"
def _extract_text(self, binary, mimetype):
text_content = ""
try:
if mimetype == "application/pdf":
text_content = self._extract_text_from_pdf(binary)
elif mimetype in {"image/png", "image/jpeg", "image/jpg"}:
text_content = self._extract_text_from_image(binary)
elif mimetype == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
text_content = self._extract_text_from_docx(binary)
elif mimetype.startswith("text/"):
text_content = binary.decode("utf-8", errors="ignore")
except Exception as exc:
_logger.exception("Document text extraction failed: %s", exc)
return (text_content or "").strip()
def _extract_text_from_pdf(self, binary):
extracted_parts = []
if PdfReader:
try:
reader = PdfReader(BytesIO(binary))
extracted_parts.extend(page.extract_text() or "" for page in reader.pages)
except Exception as exc:
_logger.warning("PdfReader extraction failed: %s", exc)
text_content = "\n".join(part for part in extracted_parts if part).strip()
if text_content:
return text_content
if convert_from_bytes and pytesseract:
try:
images = convert_from_bytes(binary, dpi=300)
return "\n".join(
pytesseract.image_to_string(image)
for image in images
).strip()
except Exception as exc:
_logger.warning("PDF OCR extraction failed: %s", exc)
return ""
def _extract_text_from_image(self, binary):
if not pytesseract or not Image:
return ""
try:
image = Image.open(BytesIO(binary))
return pytesseract.image_to_string(image).strip()
except Exception as exc:
_logger.warning("Image OCR extraction failed: %s", exc)
return ""
def _extract_text_from_docx(self, binary):
if not Document:
return ""
try:
document = Document(BytesIO(binary))
return "\n".join(
paragraph.text for paragraph in document.paragraphs if paragraph.text
).strip()
except Exception as exc:
_logger.warning("DOCX extraction failed: %s", exc)
return ""
def _send_to_ai(self, text_content, schema_text, extra_instructions=None):
prompt = self._build_prompt(text_content, schema_text, extra_instructions)
errors = []
together_key = self._get_param("document_parser.together_ai_key") or self._get_param("document_parser.together_api_key")
openrouter_key = self._get_param("document_parser.openrouter_ai_key") or self._get_param("document_parser.openrouter_api_key")
if together_key:
result, provider_errors = self._call_provider(
provider_name="Together",
endpoint=self.TOGETHER_ENDPOINT,
headers={
"Authorization": f"Bearer {together_key}",
"Content-Type": "application/json",
},
models=self.TOGETHER_MODELS,
prompt=prompt,
)
if result:
return result, "together", errors
errors.extend(provider_errors)
else:
errors.append(_("Together AI key is not configured."))
if openrouter_key:
result, provider_errors = self._call_provider(
provider_name="OpenRouter",
endpoint=self.OPENROUTER_ENDPOINT,
headers={
"Authorization": f"Bearer {openrouter_key}",
"Content-Type": "application/json",
"HTTP-Referer": self._get_param("web.base.url") or "odoo.local",
"X-Title": "Document Parser",
},
models=self.OPENROUTER_MODELS,
prompt=prompt,
)
if result:
return result, "openrouter", errors
errors.extend(provider_errors)
else:
errors.append(_("OpenRouter key is not configured."))
return {}, False, errors
def _build_prompt(self, text_content, schema_text, extra_instructions=None):
return f"""
You are a strict JSON generator.
RULES:
- Output ONLY valid raw JSON.
- No explanation.
- No markdown.
- No backticks.
- No extra text.
- Follow schema strictly.
- If a field is missing in text, return null.
- Scan the entire document carefully before answering.
- Extract ONLY what exists in text.
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
FIELD RULES:
- If "skills" exists, extract only explicit technical skills written in the document.
- Do NOT infer similar skills from role names, responsibilities, or projects.
- Normalize names like "Expert Python" to "Python".
- Exclude soft skills and business phrases.
- Exclude responsibility-style phrases like Cross-Functional Collaboration, Cost Saving, Resource Utilization, Documentation, Reporting, and Team Handling.
- Prefer concrete tools, methods, technologies, platforms, certifications, engineering/process methods, and domain techniques explicitly written in the resume.
- If the resume explicitly mentions items like AutoCAD, Root Cause Analysis, Project Management, Manufacturing Processes, Lean, Six Sigma, or Quality Control, include them.
- Remove duplicates and return each skill only once.
- If "email" exists, return one valid normalized email.
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
- If "phone" exists, return the most complete phone number found.
- If "experience" exists, return only clearly supported numeric values.
Schema:
{schema_text}
Instructions:
{extra_instructions or "None"}
Document:
{text_content}
"""
def _call_provider(self, provider_name, endpoint, headers, models, prompt):
errors = []
for model in models:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
"max_tokens": 1500,
}
try:
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)
if response.status_code != 200:
message = _("%(provider)s model %(model)s failed with %(status)s: %(body)s") % {
"provider": provider_name,
"model": model,
"status": response.status_code,
"body": (response.text or "")[:300],
}
_logger.warning(message)
errors.append(message)
continue
body = response.json()
content = self._extract_message_content(body)
parsed = self._safe_json_load(content)
if parsed:
return parsed, errors
message = _("%(provider)s model %(model)s returned invalid JSON.") % {
"provider": provider_name,
"model": model,
}
_logger.warning(message)
errors.append(message)
except Exception as exc:
message = _("%(provider)s model %(model)s error: %(error)s") % {
"provider": provider_name,
"model": model,
"error": str(exc),
}
_logger.warning(message)
errors.append(message)
return {}, errors
def _extract_message_content(self, response_body):
try:
content = response_body["choices"][0]["message"]["content"]
except Exception:
return ""
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict):
if item.get("type") == "text":
parts.append(item.get("text", ""))
elif item.get("text"):
parts.append(item.get("text"))
else:
parts.append(str(item))
return "\n".join(part for part in parts if part)
if isinstance(content, dict):
return content.get("text", "")
return content or ""
def _safe_json_load(self, content):
if not content:
return {}
content = content.strip().replace("```json", "").replace("```", "").strip()
try:
return json.loads(content)
except Exception:
pass
match = re.search(r"\{[\s\S]*\}", content)
if match:
try:
return json.loads(match.group(0))
except Exception:
pass
_logger.warning("JSON parse failed for provider response: %s", content[:500])
return {}
def _extract_with_heuristics(self, text_content, fields):
result = {}
email_match = re.search(r"([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})", text_content or "", re.I)
phone_match = re.search(r"(\+?\d[\d\-\s()]{7,}\d)", text_content or "")
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", text_content or "", re.I)
name_guess = self._guess_name(text_content or "")
skills_guess = self._guess_skills(text_content or "")
for field_name, field_spec in fields.items():
field_type = field_spec.get("type", "string")
if field_name in {"email", "email_from"}:
result[field_name] = email_match.group(1).lower() if email_match else None
elif field_name in {"phone", "mobile", "partner_phone"}:
result[field_name] = phone_match.group(1).strip() if phone_match else None
elif field_name in {"linkedin_profile", "linkedin"}:
result[field_name] = linkedin_match.group(1).strip() if linkedin_match else None
elif field_name in {"name", "full_name", "partner_name"}:
result[field_name] = name_guess
elif field_name == "skills" and field_type == "list":
result[field_name] = skills_guess
else:
result[field_name] = None
return result
def _guess_name(self, text_content):
for line in [line.strip() for line in (text_content or "").splitlines() if line.strip()][:12]:
cleaned = re.sub(r"[^A-Za-z .'-]", "", line).strip()
if len(cleaned.split()) in {2, 3, 4} and not re.search(r"(resume|cv|email|phone|linkedin|skills|experience)", cleaned, re.I):
return cleaned
return None
def _guess_skills(self, text_content):
section = re.search(r"(skills|technical skills|core competencies)(.*?)(experience|education|projects|certifications|$)", text_content or "", re.I | re.S)
if not section:
return []
parts = re.split(r"[,;\n|•]", section.group(2))
cleaned = []
for part in parts:
value = re.sub(r"\s+", " ", part).strip(" -:\t\r\n")
if value and 1 < len(value) < 50 and not re.search(r"^(skills?|experience|education)$", value, re.I):
cleaned.append(value)
return list(dict.fromkeys(cleaned[:25]))
def _get_param(self, key):
return self.env["ir.config_parameter"].sudo().get_param(key)
def _normalize_required_fields(self, fields):
if isinstance(fields, dict):
normalized = {}
for field_name, field_value in fields.items():
if isinstance(field_value, dict):
normalized[field_name] = {
"type": field_value.get("type", "string"),
"description": field_value.get("description", field_name.replace("_", " ").title()),
}
else:
normalized[field_name] = {
"type": "string",
"description": str(field_value or field_name.replace("_", " ").title()),
}
return normalized
if isinstance(fields, list):
return {field_name: {"type": "string", "description": field_name.replace("_", " ").title()} for field_name in fields}
return {}
def _build_json_schema_text(self, fields):
return json.dumps(fields, ensure_ascii=True)

View File

@ -0,0 +1,67 @@
import requests
from odoo import _, fields, models
from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
together_ai_key = fields.Char(
string="Together AI Key",
config_parameter="document_parser.together_ai_key",
)
openrouter_ai_key = fields.Char(
string="OpenRouter AI Key",
config_parameter="document_parser.openrouter_ai_key",
)
def action_test_together_ai_connection(self):
self.ensure_one()
if not self.together_ai_key:
raise UserError(_("Please add the Together AI key first."))
response = requests.get(
"https://api.together.xyz/v1/models",
headers={"Authorization": f"Bearer {self.together_ai_key}"},
timeout=20,
)
if response.ok:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Together AI Connection"),
"message": _("Connection successful."),
"type": "success",
"sticky": False,
},
}
raise UserError(_("Together AI connection failed: %s") % (response.text or response.reason))
def action_test_openrouter_ai_connection(self):
self.ensure_one()
if not self.openrouter_ai_key:
raise UserError(_("Please add the OpenRouter key first."))
response = requests.get(
"https://openrouter.ai/api/v1/models",
headers={
"Authorization": f"Bearer {self.openrouter_ai_key}",
"HTTP-Referer": self.env["ir.config_parameter"].sudo().get_param("web.base.url", ""),
"X-Title": "Odoo Document Parser",
},
timeout=20,
)
if response.ok:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("OpenRouter Connection"),
"message": _("Connection successful."),
"type": "success",
"sticky": False,
},
}
raise UserError(_("OpenRouter connection failed: %s") % (response.text or response.reason))

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_document_parser" model="ir.ui.view">
<field name="name">res.config.settings.view.form.document.parser</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="80"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="Document Parser" name="document_parser" groups="base.group_system">
<block title="AI Providers" name="document_parser_ai_provider_block">
<setting string="Together AI Key"
help="Primary provider used first for structured document extraction."
id="document_parser_together_ai_key">
<div class="d-flex align-items-center gap-2">
<field name="together_ai_key" password="True" placeholder="together.ai API key"/>
<button name="action_test_together_ai_connection"
string="Test Connection"
type="object"
class="btn btn-secondary"/>
</div>
</setting>
<setting string="OpenRouter AI Key"
help="Fallback provider used when Together AI is unavailable or quota is exhausted."
id="document_parser_openrouter_ai_key">
<div class="d-flex align-items-center gap-2">
<field name="openrouter_ai_key" password="True" placeholder="openrouter API key"/>
<button name="action_test_openrouter_ai_connection"
string="Test Connection"
type="object"
class="btn btn-secondary"/>
</div>
</setting>
</block>
</app>
</xpath>
</field>
</record>
</odoo>

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers
from . import wizard
# -*- coding: utf-8 -*-
from . import models
from . import controllers
from . import wizard

View File

@ -1,94 +1,94 @@
# -*- coding: utf-8 -*-
{
'name': "Documents",
'summary': "Collect, organize and share documents.",
'description': """
App to upload and manage your documents.
""",
'category': 'Productivity/Documents',
'sequence': 80,
'version': '1.4',
'application': True,
'website': 'https://www.ftprotech.in/',
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'portal', 'attachment_indexation', 'digest'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/digest_data.xml',
'data/mail_template_data.xml',
'data/mail_activity_type_data.xml',
'data/documents_tag_data.xml',
'data/documents_document_data.xml',
'data/ir_config_parameter_data.xml',
'data/documents_tour.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/documents_access_views.xml',
'views/documents_document_views.xml',
'views/documents_folder_views.xml',
'views/documents_tag_views.xml',
'views/mail_activity_views.xml',
'views/mail_activity_plan_views.xml',
'views/mail_alias_views.xml',
'views/documents_menu_views.xml',
'views/documents_templates_portal.xml',
'views/documents_templates_share.xml',
'wizard/documents_link_to_record_wizard_views.xml',
'wizard/documents_request_wizard_views.xml',
# Need the `ir.actions.act_window` to exist
'data/ir_actions_server_data.xml',
],
'demo': [
'demo/documents_document_demo.xml',
],
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'documents/static/src/model/**/*',
'documents/static/src/scss/documents_views.scss',
'documents/static/src/scss/documents_kanban_view.scss',
'documents/static/src/attachments/**/*',
'documents/static/src/core/**/*',
'documents/static/src/js/**/*',
'documents/static/src/owl/**/*',
'documents/static/src/views/**/*',
('remove', 'documents/static/src/views/activity/**'),
('after', 'web/static/src/core/errors/error_dialogs.xml', 'documents/static/src/web/error_dialog/error_dialog_patch.xml'),
'documents/static/src/web/**/*',
'documents/static/src/components/**/*',
],
'web.assets_backend_lazy': [
'documents/static/src/views/activity/**',
],
'web._assets_primary_variables': [
'documents/static/src/scss/documents.variables.scss',
],
"web.dark_mode_variables": [
('before', 'documents/static/src/scss/documents.variables.scss', 'documents/static/src/scss/documents.variables.dark.scss'),
],
'documents.public_page_assets': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap_backend'),
'documents/static/src/scss/documents_public_pages.scss',
],
'documents.webclient': [
('include', 'web.assets_backend'),
# documents webclient overrides
'documents/static/src/portal_webclient/**/*',
'web/static/src/start.js',
],
}
}
# -*- coding: utf-8 -*-
{
'name': "Documents",
'summary': "Collect, organize and share documents.",
'description': """
App to upload and manage your documents.
""",
'category': 'Productivity/Documents',
'sequence': 80,
'version': '1.4',
'application': True,
'website': 'https://www.ftprotech.in/',
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'portal', 'attachment_indexation', 'digest'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/digest_data.xml',
'data/mail_template_data.xml',
'data/mail_activity_type_data.xml',
'data/documents_tag_data.xml',
'data/documents_document_data.xml',
'data/ir_config_parameter_data.xml',
'data/documents_tour.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/documents_access_views.xml',
'views/documents_document_views.xml',
'views/documents_folder_views.xml',
'views/documents_tag_views.xml',
'views/mail_activity_views.xml',
'views/mail_activity_plan_views.xml',
'views/mail_alias_views.xml',
'views/documents_menu_views.xml',
'views/documents_templates_portal.xml',
'views/documents_templates_share.xml',
'wizard/documents_link_to_record_wizard_views.xml',
'wizard/documents_request_wizard_views.xml',
# Need the `ir.actions.act_window` to exist
'data/ir_actions_server_data.xml',
],
'demo': [
'demo/documents_document_demo.xml',
],
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'documents/static/src/model/**/*',
'documents/static/src/scss/documents_views.scss',
'documents/static/src/scss/documents_kanban_view.scss',
'documents/static/src/attachments/**/*',
'documents/static/src/core/**/*',
'documents/static/src/js/**/*',
'documents/static/src/owl/**/*',
'documents/static/src/views/**/*',
('remove', 'documents/static/src/views/activity/**'),
('after', 'web/static/src/core/errors/error_dialogs.xml', 'documents/static/src/web/error_dialog/error_dialog_patch.xml'),
'documents/static/src/web/**/*',
'documents/static/src/components/**/*',
],
'web.assets_backend_lazy': [
'documents/static/src/views/activity/**',
],
'web._assets_primary_variables': [
'documents/static/src/scss/documents.variables.scss',
],
"web.dark_mode_variables": [
('before', 'documents/static/src/scss/documents.variables.scss', 'documents/static/src/scss/documents.variables.dark.scss'),
],
'documents.public_page_assets': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap_backend'),
'documents/static/src/scss/documents_public_pages.scss',
],
'documents.webclient': [
('include', 'web.assets_backend'),
# documents webclient overrides
'documents/static/src/portal_webclient/**/*',
'web/static/src/start.js',
],
}
}

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from . import documents
from . import home
from . import portal
# -*- coding: utf-8 -*-
from . import documents
from . import home
from . import portal

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +1,86 @@
from http import HTTPStatus
from urllib.parse import urlencode
from odoo.http import request, route
from odoo.addons.web.controllers import home as web_home
from odoo.addons.web.controllers.utils import ensure_db
from .documents import ShareRoute
class Home(web_home.Home):
def _web_client_readonly(self):
""" Force a read/write cursor for documents.access """
path = request.httprequest.path
if (
path.startswith('/odoo/documents')
and (request.httprequest.args.get('access_token') or path.removeprefix('/odoo/documents/'))
and request.session.uid
):
return False
return super()._web_client_readonly()
@route(readonly=_web_client_readonly)
def web_client(self, s_action=None, **kw):
""" Handle direct access to a document with a backend URL (/odoo/documents/<access_token>).
It redirects to the document either in:
- the backend if the user is logged and has access to the Documents module
- or a lightweight version of the backend if the user is logged and has not access
to the Document module but well to the documents.document model
- or the document portal otherwise
Goal: Allow to share directly the backend URL of a document.
"""
subpath = kw.get('subpath', '')
access_token = request.params.get('access_token') or subpath.removeprefix('documents/')
if not subpath.startswith('documents') or not access_token or '/' in access_token:
return super().web_client(s_action, **kw)
# This controller should be auth='public' but it actually is
# auth='none' for technical reasons (see super). Those three
# lines restore the public behavior.
ensure_db()
request.update_env(user=request.session.uid)
request.env['ir.http']._authenticate_explicit('public')
# Public/Portal users use the /documents/<access_token> route
if not request.env.user._is_internal():
return request.redirect(
f'/documents/{access_token}',
HTTPStatus.TEMPORARY_REDIRECT,
)
document_sudo = ShareRoute._from_access_token(access_token, follow_shortcut=False)
if not document_sudo:
Redirect = request.env['documents.redirect'].sudo()
if document_sudo := Redirect._get_redirection(access_token):
return request.redirect(
f'/odoo/documents/{document_sudo.access_token}',
HTTPStatus.MOVED_PERMANENTLY,
)
# We want (1) the webclient renders the webclient template and load
# the document action. We also want (2) the router rewrites
# /odoo/documents/<id> to /odoo/documents/<access-token> in the
# URL.
# We redirect on /web so this override does kicks in again,
# super() is loaded and renders the normal home template. We add
# custom fragments so we can load them inside the router and
# rewrite the URL.
query = {}
if request.session.debug:
query['debug'] = request.session.debug
fragment = {
'action': request.env.ref("documents.document_action").id,
'menu_id': request.env.ref('documents.menu_root').id,
'model': 'documents.document',
}
if document_sudo:
fragment.update({
f'documents_init_{key}': value
for key, value
in ShareRoute._documents_get_init_data(document_sudo, request.env.user).items()
})
return request.redirect(f'/web?{urlencode(query)}#{urlencode(fragment)}')
from http import HTTPStatus
from urllib.parse import urlencode
from odoo.http import request, route
from odoo.addons.web.controllers import home as web_home
from odoo.addons.web.controllers.utils import ensure_db
from .documents import ShareRoute
class Home(web_home.Home):
def _web_client_readonly(self):
""" Force a read/write cursor for documents.access """
path = request.httprequest.path
if (
path.startswith('/odoo/documents')
and (request.httprequest.args.get('access_token') or path.removeprefix('/odoo/documents/'))
and request.session.uid
):
return False
return super()._web_client_readonly()
@route(readonly=_web_client_readonly)
def web_client(self, s_action=None, **kw):
""" Handle direct access to a document with a backend URL (/odoo/documents/<access_token>).
It redirects to the document either in:
- the backend if the user is logged and has access to the Documents module
- or a lightweight version of the backend if the user is logged and has not access
to the Document module but well to the documents.document model
- or the document portal otherwise
Goal: Allow to share directly the backend URL of a document.
"""
subpath = kw.get('subpath', '')
access_token = request.params.get('access_token') or subpath.removeprefix('documents/')
if not subpath.startswith('documents') or not access_token or '/' in access_token:
return super().web_client(s_action, **kw)
# This controller should be auth='public' but it actually is
# auth='none' for technical reasons (see super). Those three
# lines restore the public behavior.
ensure_db()
request.update_env(user=request.session.uid)
request.env['ir.http']._authenticate_explicit('public')
# Public/Portal users use the /documents/<access_token> route
if not request.env.user._is_internal():
return request.redirect(
f'/documents/{access_token}',
HTTPStatus.TEMPORARY_REDIRECT,
)
document_sudo = ShareRoute._from_access_token(access_token, follow_shortcut=False)
if not document_sudo:
Redirect = request.env['documents.redirect'].sudo()
if document_sudo := Redirect._get_redirection(access_token):
return request.redirect(
f'/odoo/documents/{document_sudo.access_token}',
HTTPStatus.MOVED_PERMANENTLY,
)
# We want (1) the webclient renders the webclient template and load
# the document action. We also want (2) the router rewrites
# /odoo/documents/<id> to /odoo/documents/<access-token> in the
# URL.
# We redirect on /web so this override does kicks in again,
# super() is loaded and renders the normal home template. We add
# custom fragments so we can load them inside the router and
# rewrite the URL.
query = {}
if request.session.debug:
query['debug'] = request.session.debug
fragment = {
'action': request.env.ref("documents.document_action").id,
'menu_id': request.env.ref('documents.menu_root').id,
'model': 'documents.document',
}
if document_sudo:
fragment.update({
f'documents_init_{key}': value
for key, value
in ShareRoute._documents_get_init_data(document_sudo, request.env.user).items()
})
return request.redirect(f'/web?{urlencode(query)}#{urlencode(fragment)}')

View File

@ -1,19 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.exceptions import AccessError
from odoo.http import request
class DocumentCustomerPortal(CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if 'document_count' in counters:
Document = request.env['documents.document']
try:
count = Document.search_count([])
except AccessError:
count = 0
values['document_count'] = count
return values
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.exceptions import AccessError
from odoo.http import request
class DocumentCustomerPortal(CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if 'document_count' in counters:
Document = request.env['documents.document']
try:
count = Document.search_count([])
except AccessError:
count = 0
values['document_count'] = count
return values

View File

@ -1,23 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data>
<record id="digest_tip_documents_0" model="digest.tip">
<field name="name">Tip: Become a paperless company</field>
<field name="sequence">300</field>
<field name="group_id" ref="documents.group_documents_user" />
<field name="tip_description" type="html">
<div>
<t t-set="record" t-value="object.env['documents.document'].search([('alias_name', '!=', False), ('alias_domain_id', '!=', False)], limit=1)" />
<b class="tip_title">Tip: Become a paperless company</b>
<t t-if="record.alias_email">
<p class="tip_content">An easy way to process incoming mails is to configure your scanner to send PDFs to <t t-out="record.alias_email"/>. Scanned files will appear automatically in your workspace. Then, process your documents in bulk with the split tool: launch user defined actions, request a signature, convert to vendor bills with AI, etc.</p>
</t>
<t t-else="">
<p class="tip_content">An easy way to process incoming mails is to configure your scanner to send PDFs to your workspace email. Scanned files will appear automatically in your workspace. Then, process your documents in bulk with the split tool: launch user defined actions, request a signature, convert to vendor bills with AI, etc.</p>
</t>
<img src="/documents/static/src/img/documents-paperless.png" width="540" class="illustration_border" />
</div>
</field>
</record>
</data>
</odoo>
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data>
<record id="digest_tip_documents_0" model="digest.tip">
<field name="name">Tip: Become a paperless company</field>
<field name="sequence">300</field>
<field name="group_id" ref="documents.group_documents_user" />
<field name="tip_description" type="html">
<div>
<t t-set="record" t-value="object.env['documents.document'].search([('alias_name', '!=', False), ('alias_domain_id', '!=', False)], limit=1)" />
<b class="tip_title">Tip: Become a paperless company</b>
<t t-if="record.alias_email">
<p class="tip_content">An easy way to process incoming mails is to configure your scanner to send PDFs to <t t-out="record.alias_email"/>. Scanned files will appear automatically in your workspace. Then, process your documents in bulk with the split tool: launch user defined actions, request a signature, convert to vendor bills with AI, etc.</p>
</t>
<t t-else="">
<p class="tip_content">An easy way to process incoming mails is to configure your scanner to send PDFs to your workspace email. Scanned files will appear automatically in your workspace. Then, process your documents in bulk with the split tool: launch user defined actions, request a signature, convert to vendor bills with AI, etc.</p>
</t>
<img src="/documents/static/src/img/documents-paperless.png" width="540" class="illustration_border" />
</div>
</field>
</record>
</data>
</odoo>

View File

@ -1,43 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!-- Folders -->
<record id="document_internal_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="access_internal">view</field>
<field name="name">Internal</field>
<field name="is_pinned_folder">True</field>
</record>
<record id="document_finance_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="access_internal">edit</field>
<field name="name">Finance</field>
<field name="is_pinned_folder">True</field>
</record>
<record id="document_marketing_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="name">Marketing</field>
<field name="access_internal">edit</field>
<field name="is_pinned_folder">True</field>
</record>
<record id="document_support_folder" model="documents.document" forcecreate="True">
<field name="name">Support</field>
<field name="type">folder</field>
<field name="access_internal">none</field>
<field name="access_via_link">none</field>
</record>
<!-- base data -->
<record id="documents_attachment_video_documents" model="documents.document" forcecreate="0">
<field name="name">Video: Odoo Documents</field>
<field name="type">url</field>
<field name="url">https://youtu.be/Ayab6wZ_U1A</field>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_presentations'),
ref('documents.documents_tag_validated')])]"/>
</record>
</data></odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!-- Folders -->
<record id="document_internal_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="access_internal">view</field>
<field name="name">Internal</field>
<field name="is_pinned_folder">True</field>
</record>
<record id="document_finance_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="access_internal">edit</field>
<field name="name">Finance</field>
<field name="is_pinned_folder">True</field>
</record>
<record id="document_marketing_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="name">Marketing</field>
<field name="access_internal">edit</field>
<field name="is_pinned_folder">True</field>
</record>
<record id="document_support_folder" model="documents.document" forcecreate="True">
<field name="name">Support</field>
<field name="type">folder</field>
<field name="access_internal">none</field>
<field name="access_via_link">none</field>
</record>
<!-- base data -->
<record id="documents_attachment_video_documents" model="documents.document" forcecreate="0">
<field name="name">Video: Odoo Documents</field>
<field name="type">url</field>
<field name="url">https://youtu.be/Ayab6wZ_U1A</field>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_presentations'),
ref('documents.documents_tag_validated')])]"/>
</record>
</data></odoo>

View File

@ -1,127 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!-- tags internal -->
<record id="documents_tag_draft" model="documents.tag" forcecreate="0">
<field name="name">Draft</field>
<field name="sequence">2</field>
</record>
<record id="documents_tag_inbox" model="documents.tag" forcecreate="0">
<field name="name">Inbox</field>
<field name="sequence">4</field>
</record>
<record id="documents_tag_to_validate" model="documents.tag" forcecreate="0">
<field name="name">To Validate</field>
<field name="sequence">6</field>
</record>
<record id="documents_tag_validated" model="documents.tag" forcecreate="0">
<field name="name">Validated</field>
<field name="sequence">8</field>
</record>
<record id="documents_tag_deprecated" model="documents.tag" forcecreate="0">
<field name="name">Deprecated</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_hr" model="documents.tag" forcecreate="0">
<field name="name">HR</field>
<field name="sequence">9</field>
</record>
<record id="documents_tag_sales" model="documents.tag" forcecreate="0">
<field name="name">Sales</field>
<field name="sequence">9</field>
</record>
<record id="documents_tag_legal" model="documents.tag" forcecreate="0">
<field name="name">Legal</field>
<field name="sequence">9</field>
</record>
<record id="documents_tag_other" model="documents.tag" forcecreate="0">
<field name="name">Other</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_presentations" model="documents.tag" forcecreate="0">
<field name="name">Presentations</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_contracts" model="documents.tag" forcecreate="0">
<field name="name">Contracts</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_project" model="documents.tag" forcecreate="0">
<field name="name">Project</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_text" model="documents.tag" forcecreate="0">
<field name="name">Text</field>
<field name="sequence">10</field>
</record>
<!-- tags finance -->
<record id="documents_tag_bill" model="documents.tag" forcecreate="0">
<field name="name">Bill</field>
<field name="sequence">4</field>
</record>
<record id="documents_tag_expense" model="documents.tag" forcecreate="0">
<field name="name">Expense</field>
<field name="sequence">5</field>
</record>
<record id="documents_tag_vat" model="documents.tag" forcecreate="0">
<field name="name">VAT</field>
<field name="sequence">6</field>
</record>
<record id="documents_tag_fiscal" model="documents.tag" forcecreate="0">
<field name="name">Fiscal</field>
<field name="sequence">7</field>
</record>
<record id="documents_tag_financial" model="documents.tag" forcecreate="0">
<field name="name">Financial</field>
<field name="sequence">8</field>
</record>
<record id="documents_tag_year_current" model="documents.tag" forcecreate="0">
<field name="name" eval="str(datetime.now().year)"/>
<field name="sequence">10</field>
</record>
<record id="documents_tag_year_previous" model="documents.tag" forcecreate="0">
<field name="name" eval="str(datetime.now().year-1)"/>
<field name="sequence">11</field>
</record>
<!-- tags marketing -->
<record id="documents_tag_ads" model="documents.tag" forcecreate="0">
<field name="name">Ads</field>
<field name="sequence">12</field>
</record>
<record id="documents_tag_brochures" model="documents.tag" forcecreate="0">
<field name="name">Brochures</field>
<field name="sequence">13</field>
</record>
<record id="documents_tag_images" model="documents.tag" forcecreate="0">
<field name="name">Images</field>
<field name="sequence">14</field>
</record>
<record id="documents_tag_videos" model="documents.tag" forcecreate="0">
<field name="name">Videos</field>
<field name="sequence">15</field>
</record>
</data></odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<!-- tags internal -->
<record id="documents_tag_draft" model="documents.tag" forcecreate="0">
<field name="name">Draft</field>
<field name="sequence">2</field>
</record>
<record id="documents_tag_inbox" model="documents.tag" forcecreate="0">
<field name="name">Inbox</field>
<field name="sequence">4</field>
</record>
<record id="documents_tag_to_validate" model="documents.tag" forcecreate="0">
<field name="name">To Validate</field>
<field name="sequence">6</field>
</record>
<record id="documents_tag_validated" model="documents.tag" forcecreate="0">
<field name="name">Validated</field>
<field name="sequence">8</field>
</record>
<record id="documents_tag_deprecated" model="documents.tag" forcecreate="0">
<field name="name">Deprecated</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_hr" model="documents.tag" forcecreate="0">
<field name="name">HR</field>
<field name="sequence">9</field>
</record>
<record id="documents_tag_sales" model="documents.tag" forcecreate="0">
<field name="name">Sales</field>
<field name="sequence">9</field>
</record>
<record id="documents_tag_legal" model="documents.tag" forcecreate="0">
<field name="name">Legal</field>
<field name="sequence">9</field>
</record>
<record id="documents_tag_other" model="documents.tag" forcecreate="0">
<field name="name">Other</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_presentations" model="documents.tag" forcecreate="0">
<field name="name">Presentations</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_contracts" model="documents.tag" forcecreate="0">
<field name="name">Contracts</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_project" model="documents.tag" forcecreate="0">
<field name="name">Project</field>
<field name="sequence">10</field>
</record>
<record id="documents_tag_text" model="documents.tag" forcecreate="0">
<field name="name">Text</field>
<field name="sequence">10</field>
</record>
<!-- tags finance -->
<record id="documents_tag_bill" model="documents.tag" forcecreate="0">
<field name="name">Bill</field>
<field name="sequence">4</field>
</record>
<record id="documents_tag_expense" model="documents.tag" forcecreate="0">
<field name="name">Expense</field>
<field name="sequence">5</field>
</record>
<record id="documents_tag_vat" model="documents.tag" forcecreate="0">
<field name="name">VAT</field>
<field name="sequence">6</field>
</record>
<record id="documents_tag_fiscal" model="documents.tag" forcecreate="0">
<field name="name">Fiscal</field>
<field name="sequence">7</field>
</record>
<record id="documents_tag_financial" model="documents.tag" forcecreate="0">
<field name="name">Financial</field>
<field name="sequence">8</field>
</record>
<record id="documents_tag_year_current" model="documents.tag" forcecreate="0">
<field name="name" eval="str(datetime.now().year)"/>
<field name="sequence">10</field>
</record>
<record id="documents_tag_year_previous" model="documents.tag" forcecreate="0">
<field name="name" eval="str(datetime.now().year-1)"/>
<field name="sequence">11</field>
</record>
<!-- tags marketing -->
<record id="documents_tag_ads" model="documents.tag" forcecreate="0">
<field name="name">Ads</field>
<field name="sequence">12</field>
</record>
<record id="documents_tag_brochures" model="documents.tag" forcecreate="0">
<field name="name">Brochures</field>
<field name="sequence">13</field>
</record>
<record id="documents_tag_images" model="documents.tag" forcecreate="0">
<field name="name">Images</field>
<field name="sequence">14</field>
</record>
<record id="documents_tag_videos" model="documents.tag" forcecreate="0">
<field name="name">Videos</field>
<field name="sequence">15</field>
</record>
</data></odoo>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="documents_tour" model="web_tour.tour">
<field name="name">documents_tour</field>
<field name="sequence">180</field>
<field name="rainbow_man_message"><![CDATA[
Wow... 6 documents processed in a few seconds, You're good.<br/>The tour is complete. Try uploading your own documents now.
]]></field>
</record>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="documents_tour" model="web_tour.tour">
<field name="name">documents_tour</field>
<field name="sequence">180</field>
<field name="rainbow_man_message"><![CDATA[
Wow... 6 documents processed in a few seconds, You're good.<br/>The tour is complete. Try uploading your own documents now.
]]></field>
</record>
</odoo>

View File

@ -1,106 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="ir_actions_server_create_activity" model="ir.actions.server" forcecreate="0">
<field name="name">Create Activity</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">next_activity</field>
<field name="activity_type_id" ref="documents.mail_documents_activity_data_tv"/>
</record>
<record id="ir_actions_server_remove_activities" model="ir.actions.server" forcecreate="0">
<field name="name">Mark activities as completed</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">code</field>
<field name="code">
for record in records:
record.activity_ids.action_feedback(feedback="completed")
</field>
</record>
<record id="ir_actions_server_remove_tags" model="ir.actions.server" forcecreate="0">
<field name="name">Remove all tags</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">object_write</field>
<field name="update_m2m_operation">clear</field>
<field name="update_path">tag_ids</field>
</record>
<record id="ir_actions_server_send_to_finance" model="ir.actions.server" forcecreate="0">
<field name="name">Send To Finance</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">code</field>
<field name="code">
target = env.ref('documents.document_finance_folder', raise_if_not_found=False)
if target:
permissions = records.mapped('user_permission')
records.action_move_documents(target.id)
for record, permission in zip(records, permissions):
record.sudo().action_update_access_rights(partners={env.user.partner_id: (permission, None)})
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': env._("%(nb_records)s document(s) sent to Finance", nb_records=len(records)),
'type': 'success',
}
}
</field>
</record>
<function model="documents.document" name="action_folder_embed_action" eval="[
ref('documents.document_internal_folder'),
ref('documents.ir_actions_server_send_to_finance'),
]"/>
<record id="ir_actions_server_tag_remove_inbox" model="ir.actions.server" forcecreate="0">
<field name="name">Remove Tag Inbox</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">remove</field>
<field name="resource_ref" ref="documents.documents_tag_inbox"/>
</record>
<record id="ir_actions_server_tag_remove_to_validate" model="ir.actions.server" forcecreate="0">
<field name="name">Remove Tag To Validate</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">remove</field>
<field name="resource_ref" ref="documents.documents_tag_to_validate"/>
</record>
<record id="ir_actions_server_tag_add_validated" model="ir.actions.server" forcecreate="0">
<field name="name">Add Tag Validated</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">add</field>
<field name="resource_ref" ref="documents.documents_tag_validated"/>
</record>
<record id="ir_actions_server_tag_add_bill" model="ir.actions.server" forcecreate="0">
<field name="name">Add Tag Bill</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">add</field>
<field name="resource_ref" ref="documents.documents_tag_bill"/>
</record>
</data>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="ir_actions_server_create_activity" model="ir.actions.server" forcecreate="0">
<field name="name">Create Activity</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">next_activity</field>
<field name="activity_type_id" ref="documents.mail_documents_activity_data_tv"/>
</record>
<record id="ir_actions_server_remove_activities" model="ir.actions.server" forcecreate="0">
<field name="name">Mark activities as completed</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">code</field>
<field name="code">
for record in records:
record.activity_ids.action_feedback(feedback="completed")
</field>
</record>
<record id="ir_actions_server_remove_tags" model="ir.actions.server" forcecreate="0">
<field name="name">Remove all tags</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">object_write</field>
<field name="update_m2m_operation">clear</field>
<field name="update_path">tag_ids</field>
</record>
<record id="ir_actions_server_send_to_finance" model="ir.actions.server" forcecreate="0">
<field name="name">Send To Finance</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="state">code</field>
<field name="code">
target = env.ref('documents.document_finance_folder', raise_if_not_found=False)
if target:
permissions = records.mapped('user_permission')
records.action_move_documents(target.id)
for record, permission in zip(records, permissions):
record.sudo().action_update_access_rights(partners={env.user.partner_id: (permission, None)})
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': env._("%(nb_records)s document(s) sent to Finance", nb_records=len(records)),
'type': 'success',
}
}
</field>
</record>
<function model="documents.document" name="action_folder_embed_action" eval="[
ref('documents.document_internal_folder'),
ref('documents.ir_actions_server_send_to_finance'),
]"/>
<record id="ir_actions_server_tag_remove_inbox" model="ir.actions.server" forcecreate="0">
<field name="name">Remove Tag Inbox</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">remove</field>
<field name="resource_ref" ref="documents.documents_tag_inbox"/>
</record>
<record id="ir_actions_server_tag_remove_to_validate" model="ir.actions.server" forcecreate="0">
<field name="name">Remove Tag To Validate</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">remove</field>
<field name="resource_ref" ref="documents.documents_tag_to_validate"/>
</record>
<record id="ir_actions_server_tag_add_validated" model="ir.actions.server" forcecreate="0">
<field name="name">Add Tag Validated</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">add</field>
<field name="resource_ref" ref="documents.documents_tag_validated"/>
</record>
<record id="ir_actions_server_tag_add_bill" model="ir.actions.server" forcecreate="0">
<field name="name">Add Tag Bill</field>
<field name="model_id" ref="documents.model_documents_document"/>
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
<field name="update_path">tag_ids</field>
<field name="usage">ir_actions_server</field>
<field name="state">object_write</field>
<field name="update_m2m_operation">add</field>
<field name="resource_ref" ref="documents.documents_tag_bill"/>
</record>
</data>
</odoo>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<record id="ir_config_document_upload_limit" model="ir.config_parameter">
<field name="key">document.max_fileupload_size</field>
<field name="value">67000000</field>
</record>
<record id="ir_config_deletion_delay" model="ir.config_parameter">
<field name="key">documents.deletion_delay</field>
<field name="value">30</field>
</record>
</data></odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<record id="ir_config_document_upload_limit" model="ir.config_parameter">
<field name="key">document.max_fileupload_size</field>
<field name="value">67000000</field>
</record>
<record id="ir_config_deletion_delay" model="ir.config_parameter">
<field name="key">documents.deletion_delay</field>
<field name="value">30</field>
</record>
</data></odoo>

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<record id="mail_documents_activity_data_Inbox" model="mail.activity.type">
<field name="name">Inbox</field>
<field name="res_model">documents.document</field>
</record>
<record id="mail_documents_activity_data_tv" model="mail.activity.type">
<field name="name">To validate</field>
<field name="res_model">documents.document</field>
</record>
<record id="mail_documents_activity_data_md" model="mail.activity.type">
<field name="name">Requested Document</field>
<field name="category">upload_file</field>
<field name="res_model">documents.document</field>
<field name="mail_template_ids" eval="[(4, ref('documents.mail_template_document_request_reminder'))]"/>
</record>
</data></odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<record id="mail_documents_activity_data_Inbox" model="mail.activity.type">
<field name="name">Inbox</field>
<field name="res_model">documents.document</field>
</record>
<record id="mail_documents_activity_data_tv" model="mail.activity.type">
<field name="name">To validate</field>
<field name="res_model">documents.document</field>
</record>
<record id="mail_documents_activity_data_md" model="mail.activity.type">
<field name="name">Requested Document</field>
<field name="category">upload_file</field>
<field name="res_model">documents.document</field>
<field name="mail_template_ids" eval="[(4, ref('documents.mail_template_document_request_reminder'))]"/>
</record>
</data></odoo>

View File

@ -1,252 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--Email template -->
<record id="mail_template_document_request" model="mail.template">
<field name="name">Document: Document Request</field>
<field name="model_id" ref="model_documents_document"/>
<field name="subject">Document Request {{ object.name != False and ': '+ object.name or '' }}</field>
<field name="email_to" eval="False"/>
<field name="partner_to">{{ object.requestee_partner_id.id or '' }}</field>
<field name="description">Sent to partner when requesting a document from them</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">
Document Request: <br/>
<t t-if="object.name">
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or ''">Inbox Financial</span>
</t>
</span><br/>
</td><td valign="middle" align="right" t-if="not object.create_uid.company_id.uses_default_logo">
<img t-attf-src="/logo.png?company={{ object.create_uid.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="object.create_uid.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 13px;">
<div>
Hello <t t-out="object.owner_id.name or ''">OdooBot</t>,
<br/><br/>
<t t-out="object.create_uid.name or ''">OdooBot</t> (<t t-out="object.create_uid.email or ''">odoobot@example.com</t>) asks you to provide the following document:
<br/><br/>
<center>
<div>
<t t-if="object.name">
<b t-out="object.name or ''">Inbox Financial</b>
</t>
</div>
<div>
<t t-if="object.request_activity_id.note">
<i t-out="object.request_activity_id.note or ''">Example of a note.</i>
</t>
</div>
<br/>
<div style="margin: 16px 0px 16px 0px;">
<a t-att-href="object.access_url"
style="background-color: #875A7B; padding: 20px 30px 20px 30px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Upload the requested document
</a>
</div>
</center><br/>
Please provide us with the missing document before <t t-out="object.request_activity_id.date_deadline">2021-05-17</t>.
<t t-if="user and user.signature">
<br/>
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
<br/>
</t>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="object.create_uid.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="object.create_uid.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="object.create_uid.company_id.email">
| <a t-attf-href="'mailto:%s' % {{ object.create_uid.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.email or ''">info@yourcompany.com</a>
</t>
<t t-if="object.create_uid.company_id.website">
| <a t-attf-href="'%s' % {{ object.create_uid.company_id.website }}" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.website or ''">http://www.example.com</a>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.ftprotech.in/app/documents" style="color: #875A7B;">Odoo Documents</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
<field name="lang">{{ object.owner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<template id="mail_template_document_share">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: white; padding: 0; border-collapse:separate; margin-bottom:13px;">
<tr><td valign="top">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
<t t-if="record.name">
<t t-if="record.type == 'folder'">
<t t-out="user.name or ''"/> shared this folder with you: <t t-out="record.name"/>.<br/>
</t>
<t t-else="">
<t t-out="user.name or ''"/> shared this document with you: <t t-out="record.name"/>.<br/>
</t>
</t>
<t t-elif="record.type == 'folder'">
<t t-out="user.name or ''"/> shared a folder with you.<br/>
</t>
<t t-else="">
<t t-out="user.name or ''"/> shared a document with you.<br/>
</t>
<div t-if="message" style="color:#777; margin-top:13px;" t-out="message"/>
</p>
</div>
</td></tr>
</table>
</template>
<!-- Manual reminder; copy of document request template -->
<record id="mail_template_document_request_reminder" model="mail.template">
<field name="name">Document Request: Reminder</field>
<field name="model_id" ref="model_documents_document"/>
<field name="subject">Reminder to upload your document{{ object.name and ' : ' + object.name or '' }}</field>
<field name="email_to" eval="False"/>
<field name="partner_to">{{ object.requestee_partner_id.id or '' }}</field>
<field name="description">Set reminders in activities to notify users who didn't upload their requested document</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">
Document Request: <br/>
<t t-if="object.name">
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or ''">Inbox Financial</span>
</t>
</span><br/>
</td><td valign="middle" align="right">
<img t-attf-src="/logo.png?company={{ object.create_uid.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="object.create_uid.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 13px;">
<div>
Hello <t t-out="object.owner_id.name or ''">OdooBot</t>,
<br/><br/>
This is a friendly reminder to upload your requested document:
<br/><br/>
<center>
<div>
<t t-if="object.name">
<b t-out="object.name or ''">Inbox Financial</b>
</t>
</div>
<div>
<t t-if="object.request_activity_id.note">
<i t-out="object.request_activity_id.note or ''">Example of a note.</i>
</t>
</div>
<br/>
<div style="margin: 16px 0px 16px 0px;">
<a t-att-href="object.access_url"
style="background-color: #875A7B; padding: 20px 30px 20px 30px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Upload the requested document
</a>
</div>
</center><br/>
Please provide us with the missing document before <t t-out="object.request_activity_id.date_deadline or ''">2021-05-17</t>.
<br/><br/>
Thank you,
<t t-if="user and user.signature">
<br/>
<t t-out="user.signature">--<br/>Mitchell Admin</t>
<br/>
</t>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="object.create_uid.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="object.create_uid.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="object.create_uid.company_id.email">
| <a t-attf-href="'mailto:%s' % {{ object.create_uid.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.email">info@yourcompany.com</a>
</t>
<t t-if="object.create_uid.company_id.website">
| <a t-att-href="object.create_uid.company_id.website" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.website">http://www.example.com</a>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.ftprotech.in/app/documents" style="color: #875A7B;">Odoo Documents</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
</record>
</data>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--Email template -->
<record id="mail_template_document_request" model="mail.template">
<field name="name">Document: Document Request</field>
<field name="model_id" ref="model_documents_document"/>
<field name="subject">Document Request {{ object.name != False and ': '+ object.name or '' }}</field>
<field name="email_to" eval="False"/>
<field name="partner_to">{{ object.requestee_partner_id.id or '' }}</field>
<field name="description">Sent to partner when requesting a document from them</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">
Document Request: <br/>
<t t-if="object.name">
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or ''">Inbox Financial</span>
</t>
</span><br/>
</td><td valign="middle" align="right" t-if="not object.create_uid.company_id.uses_default_logo">
<img t-attf-src="/logo.png?company={{ object.create_uid.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="object.create_uid.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 13px;">
<div>
Hello <t t-out="object.owner_id.name or ''">OdooBot</t>,
<br/><br/>
<t t-out="object.create_uid.name or ''">OdooBot</t> (<t t-out="object.create_uid.email or ''">odoobot@example.com</t>) asks you to provide the following document:
<br/><br/>
<center>
<div>
<t t-if="object.name">
<b t-out="object.name or ''">Inbox Financial</b>
</t>
</div>
<div>
<t t-if="object.request_activity_id.note">
<i t-out="object.request_activity_id.note or ''">Example of a note.</i>
</t>
</div>
<br/>
<div style="margin: 16px 0px 16px 0px;">
<a t-att-href="object.access_url"
style="background-color: #875A7B; padding: 20px 30px 20px 30px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Upload the requested document
</a>
</div>
</center><br/>
Please provide us with the missing document before <t t-out="object.request_activity_id.date_deadline">2021-05-17</t>.
<t t-if="user and user.signature">
<br/>
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
<br/>
</t>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="object.create_uid.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="object.create_uid.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="object.create_uid.company_id.email">
| <a t-attf-href="'mailto:%s' % {{ object.create_uid.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.email or ''">info@yourcompany.com</a>
</t>
<t t-if="object.create_uid.company_id.website">
| <a t-attf-href="'%s' % {{ object.create_uid.company_id.website }}" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.website or ''">http://www.example.com</a>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.ftprotech.in/app/documents" style="color: #875A7B;">Odoo Documents</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
<field name="lang">{{ object.owner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<template id="mail_template_document_share">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: white; padding: 0; border-collapse:separate; margin-bottom:13px;">
<tr><td valign="top">
<div style="margin: 0px; padding: 0px; font-size: 13px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
<t t-if="record.name">
<t t-if="record.type == 'folder'">
<t t-out="user.name or ''"/> shared this folder with you: <t t-out="record.name"/>.<br/>
</t>
<t t-else="">
<t t-out="user.name or ''"/> shared this document with you: <t t-out="record.name"/>.<br/>
</t>
</t>
<t t-elif="record.type == 'folder'">
<t t-out="user.name or ''"/> shared a folder with you.<br/>
</t>
<t t-else="">
<t t-out="user.name or ''"/> shared a document with you.<br/>
</t>
<div t-if="message" style="color:#777; margin-top:13px;" t-out="message"/>
</p>
</div>
</td></tr>
</table>
</template>
<!-- Manual reminder; copy of document request template -->
<record id="mail_template_document_request_reminder" model="mail.template">
<field name="name">Document Request: Reminder</field>
<field name="model_id" ref="model_documents_document"/>
<field name="subject">Reminder to upload your document{{ object.name and ' : ' + object.name or '' }}</field>
<field name="email_to" eval="False"/>
<field name="partner_to">{{ object.requestee_partner_id.id or '' }}</field>
<field name="description">Set reminders in activities to notify users who didn't upload their requested document</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle">
<span style="font-size: 10px;">
Document Request: <br/>
<t t-if="object.name">
<span style="font-size: 20px; font-weight: bold;" t-out="object.name or ''">Inbox Financial</span>
</t>
</span><br/>
</td><td valign="middle" align="right">
<img t-attf-src="/logo.png?company={{ object.create_uid.company_id.id }}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" t-att-alt="object.create_uid.company_id.name"/>
</td></tr>
<tr><td colspan="2" style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="top" style="font-size: 13px;">
<div>
Hello <t t-out="object.owner_id.name or ''">OdooBot</t>,
<br/><br/>
This is a friendly reminder to upload your requested document:
<br/><br/>
<center>
<div>
<t t-if="object.name">
<b t-out="object.name or ''">Inbox Financial</b>
</t>
</div>
<div>
<t t-if="object.request_activity_id.note">
<i t-out="object.request_activity_id.note or ''">Example of a note.</i>
</t>
</div>
<br/>
<div style="margin: 16px 0px 16px 0px;">
<a t-att-href="object.access_url"
style="background-color: #875A7B; padding: 20px 30px 20px 30px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
Upload the requested document
</a>
</div>
</center><br/>
Please provide us with the missing document before <t t-out="object.request_activity_id.date_deadline or ''">2021-05-17</t>.
<br/><br/>
Thank you,
<t t-if="user and user.signature">
<br/>
<t t-out="user.signature">--<br/>Mitchell Admin</t>
<br/>
</t>
</div>
</td></tr>
<tr><td style="text-align:center;">
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td></tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr><td valign="middle" align="left">
<t t-out="object.create_uid.company_id.name or ''">YourCompany</t>
</td></tr>
<tr><td valign="middle" align="left" style="opacity: 0.7;">
<t t-out="object.create_uid.company_id.phone or ''">+1 650-123-4567</t>
<t t-if="object.create_uid.company_id.email">
| <a t-attf-href="'mailto:%s' % {{ object.create_uid.company_id.email }}" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.email">info@yourcompany.com</a>
</t>
<t t-if="object.create_uid.company_id.website">
| <a t-att-href="object.create_uid.company_id.website" style="text-decoration:none; color: #454748;" t-out="object.create_uid.company_id.website">http://www.example.com</a>
</t>
</td></tr>
</table>
</td>
</tr>
</tbody>
</table>
</td></tr>
<!-- POWERED BY -->
<tr><td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
<tr><td style="text-align: center; font-size: 13px;">
Powered by <a target="_blank" href="https://www.ftprotech.in/app/documents" style="color: #875A7B;">Odoo Documents</a>
</td></tr>
</table>
</td></tr>
</table>
</field>
</record>
</data>
</odoo>

View File

@ -1,122 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(3, ref('documents.group_documents_manager'))]"/>
</record>
<!-- folders -->
<record id="document_marketing_brand1_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="folder_id" ref="document_marketing_folder"/>
<field name="access_internal">edit</field>
<field name="name">Brand 1</field>
</record>
<record id="document_marketing_brand1_shared_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="folder_id" ref="document_marketing_brand1_folder"/>
<field name="access_internal">edit</field>
<field name="access_via_link">view</field>
<field name="name">Shared</field>
</record>
<record id="document_marketing_brand2_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="folder_id" ref="document_marketing_folder"/>
<field name="access_internal">edit</field>
<field name="name">Brand 2</field>
</record>
<!-- internal -->
<record id="documents_data_multi_pdf_document" model="documents.document" forcecreate="0">
<field name="name">Mails_inbox.pdf</field>
<field name="datas" type="base64" file="documents/data/files/Mails_inbox.pdf"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<record id="documents_image_city_document" model="documents.document" forcecreate="0">
<field name="name">city.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/city.jpg"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
</record>
<record id="documents_image_mail_document" model="documents.document" forcecreate="0">
<field name="name">mail.png</field>
<field name="datas" type="base64" file="documents/data/files/mail.png"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<!-- The thumbnail is added after -->
<record id="documents_image_mail_document" model="documents.document" forcecreate="0">
<field name="thumbnail" type="base64" file="documents/data/files/mail_thumbnail.png"/>
</record>
<record id="documents_image_people_document" model="documents.document" forcecreate="0">
<field name="name">people.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/people.jpg"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
</record>
<!-- finance -->
<record id="documents_vendor_bill_inv_007" model="documents.document" forcecreate="0">
<field name="name">Invoice-INV_2018_0007.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/Invoice2018_0007.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_validated')])]"/>
</record>
<record id="documents_vendor_bill_extract_azure_interior_document" model="documents.document" forcecreate="0">
<field name="name">invoice Azure Interior.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/invoice_azure_interior.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_to_validate')])]"/>
</record>
<record id="documents_vendor_bill_extract_open_value_document" model="documents.document" forcecreate="0">
<field name="name">invoice OpenValue.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/invoice_openvalue.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<record id="documents_data_comercial_tenancy_agreement" model="documents.document" forcecreate="0">
<field name="name">Commercial-Tenancy-Agreement.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/Commercial-Tenancy-Agreement.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<!-- marketing -->
<record id="documents_image_La_landscape_document" model="documents.document" forcecreate="0">
<field name="name">LA landscape.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/la.jpg"/>
<field name="folder_id" ref="documents.document_marketing_brand1_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_images')])]"/>
</record>
<record id="documents_attachment_sorry_netsuite_document" model="documents.document" forcecreate="0">
<field name="name">Sorry Netsuite.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/sorry_netsuite.jpg"/>
<field name="folder_id" ref="documents.document_marketing_brand1_shared_folder"/>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_ads')])]"/>
<field name="access_internal">edit</field>
<field name="access_via_link">view</field>
</record>
</data>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(3, ref('documents.group_documents_manager'))]"/>
</record>
<!-- folders -->
<record id="document_marketing_brand1_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="folder_id" ref="document_marketing_folder"/>
<field name="access_internal">edit</field>
<field name="name">Brand 1</field>
</record>
<record id="document_marketing_brand1_shared_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="folder_id" ref="document_marketing_brand1_folder"/>
<field name="access_internal">edit</field>
<field name="access_via_link">view</field>
<field name="name">Shared</field>
</record>
<record id="document_marketing_brand2_folder" model="documents.document" forcecreate="0">
<field name="type">folder</field>
<field name="folder_id" ref="document_marketing_folder"/>
<field name="access_internal">edit</field>
<field name="name">Brand 2</field>
</record>
<!-- internal -->
<record id="documents_data_multi_pdf_document" model="documents.document" forcecreate="0">
<field name="name">Mails_inbox.pdf</field>
<field name="datas" type="base64" file="documents/data/files/Mails_inbox.pdf"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<record id="documents_image_city_document" model="documents.document" forcecreate="0">
<field name="name">city.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/city.jpg"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
</record>
<record id="documents_image_mail_document" model="documents.document" forcecreate="0">
<field name="name">mail.png</field>
<field name="datas" type="base64" file="documents/data/files/mail.png"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<!-- The thumbnail is added after -->
<record id="documents_image_mail_document" model="documents.document" forcecreate="0">
<field name="thumbnail" type="base64" file="documents/data/files/mail_thumbnail.png"/>
</record>
<record id="documents_image_people_document" model="documents.document" forcecreate="0">
<field name="name">people.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/people.jpg"/>
<field name="folder_id" ref="documents.document_internal_folder"/>
<field name="access_internal">view</field>
</record>
<!-- finance -->
<record id="documents_vendor_bill_inv_007" model="documents.document" forcecreate="0">
<field name="name">Invoice-INV_2018_0007.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/Invoice2018_0007.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_validated')])]"/>
</record>
<record id="documents_vendor_bill_extract_azure_interior_document" model="documents.document" forcecreate="0">
<field name="name">invoice Azure Interior.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/invoice_azure_interior.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_to_validate')])]"/>
</record>
<record id="documents_vendor_bill_extract_open_value_document" model="documents.document" forcecreate="0">
<field name="name">invoice OpenValue.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/invoice_openvalue.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<record id="documents_data_comercial_tenancy_agreement" model="documents.document" forcecreate="0">
<field name="name">Commercial-Tenancy-Agreement.pdf</field>
<field name="datas" type="base64" file="documents/demo/files/Commercial-Tenancy-Agreement.pdf"/>
<field name="folder_id" ref="documents.document_finance_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_inbox')])]"/>
</record>
<!-- marketing -->
<record id="documents_image_La_landscape_document" model="documents.document" forcecreate="0">
<field name="name">LA landscape.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/la.jpg"/>
<field name="folder_id" ref="documents.document_marketing_brand1_folder"/>
<field name="access_internal">edit</field>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_images')])]"/>
</record>
<record id="documents_attachment_sorry_netsuite_document" model="documents.document" forcecreate="0">
<field name="name">Sorry Netsuite.jpg</field>
<field name="datas" type="base64" file="documents/demo/files/sorry_netsuite.jpg"/>
<field name="folder_id" ref="documents.document_marketing_brand1_shared_folder"/>
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_tag_ads')])]"/>
<field name="access_internal">edit</field>
<field name="access_via_link">view</field>
</record>
</data>
</odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More