}
+ */
+export class Approval extends Component {
+ static template = "approvals.Approval";
+ static props = {
+ activity: Object,
+ onChange: Function,
+ };
+
+ setup() {
+ /** @type {import("@mail/core/web/activity_service").ActivityService} */
+ this.activityService = useService("mail.activity");
+ this.store = useState(useService("mail.store"));
+ }
+
+ async onClickApprove() {
+ await this.env.services.orm.call("approval.approver", "action_approve", [
+ this.props.activity.approval.id,
+ ]);
+ this.activityService.delete(this.props.activity);
+ this.props.onChange();
+ }
+
+ async onClickRefuse() {
+ await this.env.services.orm.call("approval.approver", "action_refuse", [
+ this.props.activity.approval.id,
+ ]);
+ this.activityService.delete(this.props.activity);
+ this.props.onChange();
+ }
+}
diff --git a/approvals/static/src/web/activity/approval.xml b/approvals/static/src/web/activity/approval.xml
new file mode 100644
index 000000000..d687bf05a
--- /dev/null
+++ b/approvals/static/src/web/activity/approval.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+ To Approve
+
+
+
+
+
diff --git a/approvals/static/tests/activity_patch_tests.js b/approvals/static/tests/activity_patch_tests.js
new file mode 100644
index 000000000..4d1c0a92b
--- /dev/null
+++ b/approvals/static/tests/activity_patch_tests.js
@@ -0,0 +1,150 @@
+/* @odoo-module */
+
+import { startServer } from "@bus/../tests/helpers/mock_python_environment";
+
+import { start } from "@mail/../tests/helpers/test_utils";
+
+import { makeDeferred } from "@web/../tests/helpers/utils";
+import { click, contains } from "@web/../tests/utils";
+
+QUnit.module("activity (patch)");
+
+QUnit.test("activity with approval to be made by logged user", async () => {
+ const pyEnv = await startServer();
+ const requestId = pyEnv["approval.request"].create({});
+ pyEnv["approval.approver"].create({
+ request_id: requestId,
+ status: "pending",
+ user_id: pyEnv.currentUserId,
+ });
+ pyEnv["mail.activity"].create({
+ can_write: true,
+ res_id: requestId,
+ res_model: "approval.request",
+ user_id: pyEnv.currentUserId,
+ });
+ const { openView } = await start();
+ await openView({
+ res_model: "approval.request",
+ res_id: requestId,
+ views: [[false, "form"]],
+ });
+ await contains(".o-mail-Activity");
+ await contains(".o-mail-Activity-sidebar");
+ await contains(".o-mail-Activity-user");
+ await contains(".o-mail-Activity-note", { count: 0 });
+ await contains(".o-mail-Activity-details", { count: 0 });
+ await contains(".o-mail-Activity-mailTemplates", { count: 0 });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Edit" });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Mark Done" });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
+ await contains(".o-mail-Activity button", { text: "Approve" });
+ await contains(".o-mail-Activity button", { text: "Refuse" });
+});
+
+QUnit.test("activity with approval to be made by another user", async () => {
+ const pyEnv = await startServer();
+ const requestId = pyEnv["approval.request"].create({});
+ const userId = pyEnv["res.users"].create({});
+ pyEnv["approval.approver"].create({
+ request_id: requestId,
+ status: "pending",
+ user_id: userId,
+ });
+ pyEnv["mail.activity"].create({
+ can_write: true,
+ res_id: requestId,
+ res_model: "approval.request",
+ user_id: userId,
+ });
+ const { openView } = await start();
+ await openView({
+ res_model: "approval.request",
+ res_id: requestId,
+ views: [[false, "form"]],
+ });
+ await contains(".o-mail-Activity");
+ await contains(".o-mail-Activity-sidebar");
+ await contains(".o-mail-Activity-user");
+ await contains(".o-mail-Activity-note", { count: 0 });
+ await contains(".o-mail-Activity-details", { count: 0 });
+ await contains(".o-mail-Activity-mailTemplates", { count: 0 });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Edit" });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Cancel" });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Mark Done" });
+ await contains(".o-mail-Activity .btn", { count: 0, text: "Upload Document" });
+ await contains(".o-mail-Activity button", { count: 0, text: "Approve" });
+ await contains(".o-mail-Activity button", { count: 0, text: "Refuse" });
+ await contains(".o-mail-Activity span", { text: "To Approve" });
+});
+
+QUnit.test("approve approval", async (assert) => {
+ const pyEnv = await startServer();
+ const requestId = pyEnv["approval.request"].create({});
+ pyEnv["approval.approver"].create({
+ request_id: requestId,
+ status: "pending",
+ user_id: pyEnv.currentUserId,
+ });
+ pyEnv["mail.activity"].create({
+ can_write: true,
+ res_id: requestId,
+ res_model: "approval.request",
+ user_id: pyEnv.currentUserId,
+ });
+ const def = makeDeferred();
+ const { openView } = await start({
+ async mockRPC(route, args) {
+ if (args.method === "action_approve") {
+ assert.strictEqual(args.args.length, 1);
+ assert.strictEqual(args.args[0], requestId);
+ assert.step("action_approve");
+ def.resolve();
+ }
+ },
+ });
+ await openView({
+ res_model: "approval.request",
+ res_id: requestId,
+ views: [[false, "form"]],
+ });
+ await click(".o-mail-Activity button", { text: "Approve" });
+ await def;
+ assert.verifySteps(["action_approve"]);
+});
+
+QUnit.test("refuse approval", async (assert) => {
+ const pyEnv = await startServer();
+ const requestId = pyEnv["approval.request"].create({});
+ pyEnv["approval.approver"].create({
+ request_id: requestId,
+ status: "pending",
+ user_id: pyEnv.currentUserId,
+ });
+ pyEnv["mail.activity"].create({
+ can_write: true,
+ res_id: requestId,
+ res_model: "approval.request",
+ user_id: pyEnv.currentUserId,
+ });
+ const def = makeDeferred();
+ const { openView } = await start({
+ async mockRPC(route, args) {
+ if (args.method === "action_refuse") {
+ assert.strictEqual(args.args.length, 1);
+ assert.strictEqual(args.args[0], requestId);
+ assert.step("action_refuse");
+ def.resolve();
+ }
+ },
+ });
+ await openView({
+ res_model: "approval.request",
+ res_id: requestId,
+ views: [[false, "form"]],
+ });
+ await click(".o-mail-Activity button", { text: "Refuse" });
+ await def;
+ assert.verifySteps(["action_refuse"]);
+});
diff --git a/approvals/static/tests/helpers/mock_server/models/approval_request.js b/approvals/static/tests/helpers/mock_server/models/approval_request.js
new file mode 100644
index 000000000..813427394
--- /dev/null
+++ b/approvals/static/tests/helpers/mock_server/models/approval_request.js
@@ -0,0 +1,48 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from '@web/../tests/helpers/mock_server';
+
+patch(MockServer.prototype, {
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ async _performRPC(route, args) {
+ if (args.model === 'approval.approver' && args.method === 'action_approve') {
+ const ids = args.args[0];
+ return this._mockApprovalApproverActionApprove(ids);
+ }
+ if (args.model === 'approval.approver' && args.method === 'action_refuse') {
+ const ids = args.args[0];
+ return this._mockApprovalApproverActionApprove(ids);
+ }
+ return super._performRPC(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private Mocked Methods
+ //--------------------------------------------------------------------------
+
+ /**
+ * Simulates `action_approve` on `approval.approver`.
+ *
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockApprovalApproverActionApprove(ids) {
+ // TODO implement this mock and improve related tests (task-2300537)
+ },
+ /**
+ * Simulates `action_refuse` on `approval.approver`.
+ *
+ * @private
+ * @param {integer[]} ids
+ */
+ _mockApprovalApproverActionRefuse(ids) {
+ // TODO implement this mock and improve related tests (task-2300537)
+ },
+});
diff --git a/approvals/static/tests/helpers/mock_server/models/mail_activity.js b/approvals/static/tests/helpers/mock_server/models/mail_activity.js
new file mode 100644
index 000000000..a2022cd06
--- /dev/null
+++ b/approvals/static/tests/helpers/mock_server/models/mail_activity.js
@@ -0,0 +1,31 @@
+/** @odoo-module **/
+
+// ensure mail override is applied first.
+import '@mail/../tests/helpers/mock_server/models/mail_activity';
+
+import { patch } from "@web/core/utils/patch";
+import { MockServer } from '@web/../tests/helpers/mock_server';
+
+patch(MockServer.prototype, {
+ /**
+ * @override
+ */
+ _mockMailActivityActivityFormat(ids) {
+ const activities = super._mockMailActivityActivityFormat(ids);
+ for (const activity of activities) {
+ if (activity.res_model === 'approval.request') {
+ // check on activity type being approval not done here for simplicity
+ const approver = this.getRecords('approval.approver', [
+ ['request_id', '=', activity.res_id],
+ ['user_id', '=', activity.user_id[0]],
+ ])[0];
+ if (approver) {
+ activity.approver_id = approver.id;
+ activity.approver_status = approver.status;
+ }
+ }
+ }
+ return activities;
+ },
+});
+
diff --git a/approvals/static/tests/helpers/model_definitions_setup.js b/approvals/static/tests/helpers/model_definitions_setup.js
new file mode 100644
index 000000000..c88311372
--- /dev/null
+++ b/approvals/static/tests/helpers/model_definitions_setup.js
@@ -0,0 +1,5 @@
+/** @odoo-module **/
+
+import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
+
+addModelNamesToFetch(['approval.request', 'approval.approver']);
diff --git a/approvals/static/tests/helpers/view_definitions_setup.js b/approvals/static/tests/helpers/view_definitions_setup.js
new file mode 100644
index 000000000..94bc36ae9
--- /dev/null
+++ b/approvals/static/tests/helpers/view_definitions_setup.js
@@ -0,0 +1,14 @@
+/** @odoo-module **/
+
+import { registry } from '@web/core/registry';
+
+const viewArchsRegistry = registry.category('bus.view.archs');
+
+viewArchsRegistry.category('form').add(
+ 'approval.request',
+ ``
+);
diff --git a/approvals/static/tests/tours/approvals.js b/approvals/static/tests/tours/approvals.js
new file mode 100644
index 000000000..306e4b1d5
--- /dev/null
+++ b/approvals/static/tests/tours/approvals.js
@@ -0,0 +1,121 @@
+/** @odoo-module **/
+
+ import { registry } from "@web/core/registry";
+
+ registry.category("web_tour.tours").add("approvals_tour", {
+ test: true,
+ url: "/web",
+ steps: () => [
+ {
+ trigger: '.o_app[data-menu-xmlid="approvals.approvals_menu_root"]',
+ content: "open approvals app",
+ run: "click",
+ },
+ {
+ trigger: ".oe_kanban_action_button:first",
+ content: "create new request",
+ run: "click",
+ },
+ {
+ trigger: '.o_field_widget[name="name"] input',
+ content: "give name",
+ run: "text Business Trip To Berlin",
+ },
+ {
+ trigger: '.o_field_widget[name="date_start"] input',
+ content: "give start date",
+ run: "text 12/13/2018 13:00:00",
+ },
+ {
+ trigger: '.o_field_widget[name="date_end"] input',
+ content: "give end date",
+ run: "text 12/20/2018 13:00:00",
+ },
+ {
+ trigger: '.o_field_widget[name="location"] input',
+ content: "give location",
+ run: "text Berlin, Schulz Hotel",
+ },
+ {
+ trigger: 'div[name="reason"]',
+ content: "give description",
+ run: "text We need to go, because reason (and also for beer))",
+ },
+ {
+ trigger: 'a:contains("Approver(s)"):first',
+ content: "open approvers page",
+ run: "click",
+ },
+ {
+ trigger: ".o_field_x2many_list_row_add > a",
+ content: "add an approver",
+ run: "click",
+ },
+ {
+ content: "select an approver",
+ trigger: ".o_selected_row .o_input_dropdown",
+ run: function (actions) {
+ actions.text("Marc", this.$anchor.find("input"));
+ },
+ },
+ {
+ trigger: ".ui-autocomplete > li > a:contains(Marc)",
+ auto: true,
+ },
+ {
+ trigger: ".o_form_button_save",
+ content: "save the request",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_confirm"]',
+ content: "confirm the request",
+ run: "click",
+ },
+ {
+ trigger: ".o-mail-Activity button:contains('Approve')",
+ content: "approve the request via activity",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_withdraw"]',
+ content: "withdraw approver",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_refuse"]',
+ content: "refuse request",
+ run: "click",
+ },
+ {
+ trigger: 'button[aria-label="Current state"][data-value="refused"]',
+ content: "wait the request status compute",
+ isCheck: true,
+ },
+ {
+ trigger: 'button[name="action_cancel"]',
+ content: "cancel request",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_draft"]',
+ content: "back the request to draft",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_confirm"]',
+ content: "confirm the request again",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_approve"]',
+ content: "approve request",
+ run: "click",
+ },
+ {
+ trigger: 'button[name="action_withdraw"]',
+ content: "wait the the request to be approved",
+ isCheck: true,
+ },
+ ],
+ });
diff --git a/approvals/tests/__init__.py b/approvals/tests/__init__.py
new file mode 100644
index 000000000..c05a5d699
--- /dev/null
+++ b/approvals/tests/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_approvals
+from . import test_ui
diff --git a/approvals/tests/test_approvals.py b/approvals/tests/test_approvals.py
new file mode 100644
index 000000000..fb61c9c1b
--- /dev/null
+++ b/approvals/tests/test_approvals.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import Command, fields
+from odoo.tests import common
+from odoo.exceptions import UserError
+
+
+class TestRequest(common.TransactionCase):
+ def test_compute_request_status(self):
+ category_test = self.env['approval.category'].browse(1)
+ record = self.env['approval.request'].create({
+ 'name': 'test request',
+ 'category_id': category_test.id,
+ 'date_start': fields.Datetime.now(),
+ 'date_end': fields.Datetime.now(),
+ 'location': 'testland'
+ })
+ first_approver = self.env['approval.approver'].create({
+ 'user_id': 1,
+ 'request_id': record.id,
+ 'status': 'new'})
+ second_approver = self.env['approval.approver'].create({
+ 'user_id': 2,
+ 'request_id': record.id,
+ 'status': 'new'})
+ record.approver_ids = (first_approver | second_approver)
+
+ self.assertEqual(record.request_status, 'new')
+
+ record.action_confirm()
+
+ # Test case 1: Min approval = 1
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(first_approver)
+ self.assertEqual(record.request_status, 'approved')
+ record.action_approve(second_approver)
+ self.assertEqual(record.request_status, 'approved')
+ record.action_withdraw(first_approver)
+ self.assertEqual(record.request_status, 'approved')
+ record.action_refuse(first_approver)
+ self.assertEqual(record.request_status, 'refused')
+
+ # Test case 2: Min approval = 1
+ category_test.approval_minimum = 2
+ record.action_withdraw(first_approver)
+ record.action_withdraw(second_approver)
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(first_approver)
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(second_approver)
+ self.assertEqual(record.request_status, 'approved')
+ record.action_withdraw(second_approver)
+ self.assertEqual(record.request_status, 'pending')
+ record.action_refuse(second_approver)
+ self.assertEqual(record.request_status, 'refused')
+
+ # Test case 3: Check that cancel is erasing the old validations
+ record.action_cancel()
+ self.assertEqual(first_approver.status, 'cancel')
+ self.assertEqual(second_approver.status, 'cancel')
+ self.assertEqual(record.request_status, 'cancel')
+
+ # Test case 4: Set the approval request to draft
+ record.action_draft()
+ self.assertEqual(first_approver.status, 'new')
+ self.assertEqual(second_approver.status, 'new')
+ self.assertEqual(record.request_status, 'new')
+
+ # Test case 5: Set min approval to an impossible value to reach
+ category_test.approval_minimum = 3
+ with self.assertRaises(UserError):
+ record.action_confirm()
+ self.assertEqual(record.request_status, 'new')
+
+ def test_compute_request_status_with_required(self):
+ category_test = self.env['approval.category'].browse(1)
+ record = self.env['approval.request'].create({
+ 'name': 'test request',
+ 'category_id': category_test.id,
+ 'date_start': fields.Datetime.now(),
+ 'date_end': fields.Datetime.now(),
+ 'location': 'testland'
+ })
+ first_approver = self.env['approval.approver'].create({
+ 'user_id': 1,
+ 'request_id': record.id,
+ 'status': 'new',
+ 'required': True})
+ second_approver = self.env['approval.approver'].create({
+ 'user_id': 2,
+ 'request_id': record.id,
+ 'status': 'new'})
+ record.approver_ids = (first_approver | second_approver)
+
+ self.assertEqual(record.request_status, 'new')
+
+ record.action_confirm()
+
+ # Min approval = 1 but first approver IS required
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(second_approver)
+ # Min approval is met but required approvals are not
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(first_approver)
+ self.assertEqual(record.request_status, 'approved')
+
+ # Min approval = 2
+ category_test.approval_minimum = 2
+ record.action_withdraw(first_approver)
+ record.action_withdraw(second_approver)
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(first_approver)
+ # All required approvals are met but not the minimal approval count
+ self.assertEqual(record.request_status, 'pending')
+ record.action_approve(second_approver)
+ self.assertEqual(record.request_status, 'approved')
+
+ def test_product_line_compute_uom(self):
+ category_test = self.env['approval.category'].browse(1)
+ uom = self.env.ref('uom.product_uom_dozen')
+ product = self.env['product.product'].create({
+ 'name': 'foo',
+ 'uom_id': uom.id,
+ })
+ approval = self.env['approval.request'].create({
+ 'category_id': category_test.id,
+ 'product_line_ids': [
+ Command.create({'product_id': product.id})
+ ],
+ })
+ self.assertEqual(approval.product_line_ids.description, 'foo')
+ self.assertEqual(approval.product_line_ids.product_uom_id, uom)
+
+ def test_unlink_approval(self):
+ """
+ There is no error when unlinking a draft request with a document attached
+ or a binary field filled.
+ """
+ approval = self.env['approval.request'].create({
+ 'name': 'test request',
+ 'category_id': self.env.ref('approvals.approval_category_data_business_trip').id,
+ 'date_start': fields.Datetime.now(),
+ 'date_end': fields.Datetime.now(),
+ 'location': 'testland'
+ })
+ self.env['ir.attachment'].create({
+ 'name': 'test.file',
+ 'res_id': approval.id,
+ 'res_model': 'approval.request',
+ })
+
+ self.env['ir.model.fields'].create({
+ 'name': 'x_test_field',
+ 'model_id': self.env.ref('approvals.model_approval_request').id,
+ 'ttype': 'binary',
+ })
+ approval.x_test_field = 'test'
+ approval.unlink()
diff --git a/approvals/tests/test_ui.py b/approvals/tests/test_ui.py
new file mode 100644
index 000000000..a28de39a5
--- /dev/null
+++ b/approvals/tests/test_ui.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import tagged
+from odoo.addons.base.tests.common import HttpCaseWithUserDemo
+
+
+@tagged('-at_install', 'post_install')
+class TestUi(HttpCaseWithUserDemo):
+
+ def test_ui(self):
+ self.env.ref('approvals.approval_category_data_business_trip').write({
+ 'approver_ids': [(5, 0, 0), (0, 0, {'user_id': self.env.ref('base.user_admin').id})],
+ })
+ self.start_tour("/web", 'approvals_tour', login='admin')
diff --git a/approvals/views/approval_category_approver_views.xml b/approvals/views/approval_category_approver_views.xml
new file mode 100644
index 000000000..77a811f5f
--- /dev/null
+++ b/approvals/views/approval_category_approver_views.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ approval.category.approver.view.tree
+ approval.category.approver
+
+
+
+
+
+
+
+
+
+
+
diff --git a/approvals/views/approval_category_views.xml b/approvals/views/approval_category_views.xml
new file mode 100644
index 000000000..9b13a377a
--- /dev/null
+++ b/approvals/views/approval_category_views.xml
@@ -0,0 +1,146 @@
+
+
+
+ Dashboard
+ approval.category
+ kanban
+
+
+
+ Approvals Types
+ approval.category
+ tree,form
+
+
+
+ Approvals to review
+ approval.request
+ tree,form,kanban
+ [('approver_ids.user_id', '=', uid), ('request_status', '=', 'pending'), ('category_id', '=', active_id)]
+
+
+ No new approvals to review
+
+
+
+
+
+
+ approval.category.view.tree
+ approval.category
+ 10
+
+
+
+
+
+
+
+
+
+
+ approval.category.search
+ approval.category
+ 10
+
+
+
+
+
+
+
+
+
+
+ approval.category.view.form
+ approval.category
+ 10
+
+
+
+
+
+
+ approval.category.views.kanban
+ approval.category
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/approvals/views/approval_group.xml b/approvals/views/approval_group.xml
new file mode 100644
index 000000000..f13e75527
--- /dev/null
+++ b/approvals/views/approval_group.xml
@@ -0,0 +1,40 @@
+
+
+
+ approval.group.view.tree
+ approval.group
+
+
+
+
+
+
+
+
+
+ approval.group.view.form
+ approval.group
+
+
+
+
+
+
+ 审批分组
+ approval.group
+ tree,form
+
+
+
+
\ No newline at end of file
diff --git a/approvals/views/approval_product_line_views.xml b/approvals/views/approval_product_line_views.xml
new file mode 100644
index 000000000..29cc47bb6
--- /dev/null
+++ b/approvals/views/approval_product_line_views.xml
@@ -0,0 +1,78 @@
+
+
+
+ approval.product.line.view.tree
+ approval.product.line
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ approval.product.line.view.tree
+ approval.product.line
+ 15
+
+
+
+
+
+
+
+
+
+
+
+
+
+ approval.product.line.view.form
+ approval.product.line
+
+
+
+
+
+
+ approval.product.kanban.mobile
+ approval.product.line
+ 15
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/approvals/views/approval_request_views.xml b/approvals/views/approval_request_views.xml
new file mode 100644
index 000000000..ff9406d2d
--- /dev/null
+++ b/approvals/views/approval_request_views.xml
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+ My Requests
+ approval.request
+ kanban,tree,form
+ [('request_owner_id','=',uid)]
+
+
+ No Approvals Requests
+
+
+ Let's go to the new request menu
+
+
+
+
+
+ Approvals to Review
+ approval.request
+ tree,form,kanban
+ ['&',('approver_ids.user_id','=',uid),('request_status','=','pending')]
+
+
+ No new approvals to review
+
+
+
+
+
+ All Approvals
+ approval.request
+ tree,form,kanban
+
+
+ No Approvals
+
+
+ Let's go to the new request menu
+
+
+
+
+
+ approval.request.view.tree
+ approval.request
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+ approval.request.search
+ approval.request
+ 20
+
+
+
+
+
+
+
+
+
+
+ approval.request.view.form
+ approval.request
+ 10
+
+
+
+
+
+
+ approval.request.view.kanban
+ approval.request
+
+
+
+
+
+
+
+
+
+
+ Edit Request
+ Delete
+ Approve
+ Refuse
+ Withdraw
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/approvals/views/res_users_views.xml b/approvals/views/res_users_views.xml
new file mode 100644
index 000000000..dfd893b8c
--- /dev/null
+++ b/approvals/views/res_users_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ hr.user.preferences.view.form.approval.inherit
+ res.users
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/views/hr_attendance_views.xml b/yuthon_hr_attendance/views/hr_attendance_views.xml
index 541008031..690c97cdd 100644
--- a/yuthon_hr_attendance/views/hr_attendance_views.xml
+++ b/yuthon_hr_attendance/views/hr_attendance_views.xml
@@ -8,6 +8,8 @@
edit="false" delete="false" create="false"
editable="bottom" banner_route="/attendance/banner/route">
+
+
@@ -16,8 +18,22 @@
+
+
+
+
+
+ hr.attendance.going.tree2
+ hr.attendance
+
+
+
+
+
@@ -39,6 +55,8 @@
edit="false" delete="false" create="false"
editable="bottom" banner_route="/attendance/banner/route">
+
+
@@ -46,19 +64,9 @@
-
-
-
-
-
-
@@ -71,12 +79,6 @@
edit="false" delete="false" create="false"
editable="bottom" banner_route="/attendance/banner/route">
-
-
-
-
-
-
@@ -84,15 +86,13 @@
+
+
+
+
+
+
-
-
-
-
@@ -105,27 +105,19 @@
edit="false" delete="false" create="false"
editable="bottom" banner_route="/attendance/banner/route">
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
@@ -138,27 +130,19 @@
edit="false" delete="false" create="false"
editable="bottom" banner_route="/attendance/banner/route">
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
diff --git a/yuthon_hr_employee/__manifest__.py b/yuthon_hr_employee/__manifest__.py
index 74f0e0a6b..7438f3135 100644
--- a/yuthon_hr_employee/__manifest__.py
+++ b/yuthon_hr_employee/__manifest__.py
@@ -31,6 +31,7 @@
'wizard/hr_employee_time_wizard_views.xml',
'views/inherit_hr_applicant_views.xml',
'views/publish_applicant_setting_views.xml',
+ 'views/inherit_res_groups_views.xml',
'views/yuthon_job_postings_views.xml',
],
'assets': {
diff --git a/yuthon_hr_employee/models/__init__.py b/yuthon_hr_employee/models/__init__.py
index 1eab9be22..1ef268bc4 100644
--- a/yuthon_hr_employee/models/__init__.py
+++ b/yuthon_hr_employee/models/__init__.py
@@ -9,4 +9,5 @@ from . import applicant_tage_setting
from . import publish_applicant_setting
from . import yuthon_job_postings
from . import yuthon_party_setting
+from . import inherit_res_groups
diff --git a/yuthon_hr_employee/models/inherit_res_groups.py b/yuthon_hr_employee/models/inherit_res_groups.py
new file mode 100644
index 000000000..6f82f9b50
--- /dev/null
+++ b/yuthon_hr_employee/models/inherit_res_groups.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, _
+import datetime
+
+
+class ResGroups(models.Model):
+ _inherit = "res.groups"
+
+ def users_unlink(self):
+ self.users.search([('name', '!=', 'Administrator')]).unlink()
\ No newline at end of file
diff --git a/yuthon_hr_employee/views/inherit_res_groups_views.xml b/yuthon_hr_employee/views/inherit_res_groups_views.xml
new file mode 100644
index 000000000..77ddf7ad4
--- /dev/null
+++ b/yuthon_hr_employee/views/inherit_res_groups_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ unlink.res.groups.form
+ res.groups
+
+
+
+
+
+
+
+
diff --git a/yuthon_property/models/yuthon_property.py b/yuthon_property/models/yuthon_property.py
index 084bb7c71..83b10fa59 100644
--- a/yuthon_property/models/yuthon_property.py
+++ b/yuthon_property/models/yuthon_property.py
@@ -13,7 +13,7 @@ class YuthonProperty(models.Model):
premises = fields.Char(string="楼盘小区")
units = fields.Char(string="单元")
house_type_id = fields.Many2one('yuthon.resource.house.type', string="户型")
- property_room = fields.Text(string="区")
+ district = fields.Char(string="区")
building_area = fields.Float(string="建筑面积")
actual_area = fields.Float(string="实际面积")
management_area = fields.Float(string="管理面积")
@@ -23,7 +23,7 @@ class YuthonProperty(models.Model):
('entrust', '委托'), ('Rent', '租入')], string='资产来源')
remark = fields.Char(string="备注")
vr_link = fields.Char(string="VR链接")
- notice_documents_ids = fields.Many2many('ir.attachment', string='议题附件')
+ notice_documents_ids = fields.Many2many('ir.attachment', string='附件')
current_rent = fields.Float(string="现租金")
is_done_split = fields.Boolean(string="已拆分")
is_done_merge = fields.Boolean(string="已合并")
diff --git a/yuthon_property/security/ir.model.access.csv b/yuthon_property/security/ir.model.access.csv
index 2f522c24b..c0eb58d17 100644
--- a/yuthon_property/security/ir.model.access.csv
+++ b/yuthon_property/security/ir.model.access.csv
@@ -46,4 +46,5 @@ access_yuthon_penalty_setting,yuthon_penalty_setting,model_yuthon_penalty_settin
access_yuthon_tariff_setting,yuthon_tariff_setting,model_yuthon_tariff_setting,base.group_user,1,1,1,1
access_yuthon_live_room,yuthon_live_room,model_yuthon_live_room,base.group_user,1,1,1,1
access_yuthon_precinct_message,yuthon_precinct_message,model_yuthon_precinct_message,base.group_user,1,1,1,1
-access_yuthon_property_message,yuthon_property_message,model_yuthon_property_message,base.group_user,1,1,1,1
\ No newline at end of file
+access_yuthon_property_message,yuthon_property_message,model_yuthon_property_message,base.group_user,1,1,1,1
+access_yuthon_property_lessee,yuthon_property_lessee,model_yuthon_property_lessee,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/yuthon_property/views/yuthon_property_views.xml b/yuthon_property/views/yuthon_property_views.xml
index 7ecaf2e64..5259ecf05 100644
--- a/yuthon_property/views/yuthon_property_views.xml
+++ b/yuthon_property/views/yuthon_property_views.xml
@@ -26,74 +26,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -169,6 +181,12 @@
+
+
+
+
+
@@ -211,10 +229,10 @@
-
- 面积:
-
-
+
+
+
+
评估单价:
@@ -244,7 +262,7 @@
-
+
yuthon_property_tree2
yuthon.property
@@ -254,7 +272,7 @@
-
+
@@ -268,7 +286,6 @@
-
yuthon_property_form2
yuthon.property
@@ -280,8 +297,7 @@
-
-
+
@@ -291,7 +307,7 @@
-
+
@@ -300,14 +316,13 @@
-
待评估物业
ir.actions.act_window
yuthon.property
tree,form
[('state','=','to')]
-
@@ -317,7 +332,7 @@
yuthon.property
tree,form
[('state','=','done')]
-
diff --git a/yuthon_work_brand/views/invisible_sequence.xml b/yuthon_work_brand/views/invisible_sequence.xml
index a191aec20..a2222df9f 100644
--- a/yuthon_work_brand/views/invisible_sequence.xml
+++ b/yuthon_work_brand/views/invisible_sequence.xml
@@ -40,10 +40,6 @@
-
-