Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,6 @@
$(function () {
$('input.o-purchase-datetimepicker').datetimepicker();
$('input.o-purchase-datetimepicker').on("hide.datetimepicker", function () {
$(this).parents('form').submit();
});
})

View file

@ -0,0 +1,112 @@
odoo.define('purchase.PurchasePortalSidebar', function (require) {
'use strict';
var publicWidget = require('web.public.widget');
var PortalSidebar = require('portal.PortalSidebar');
publicWidget.registry.PurchasePortalSidebar = PortalSidebar.extend({
selector: '.o_portal_purchase_sidebar',
/**
* @constructor
*/
init: function (parent, options) {
this._super.apply(this, arguments);
this.authorizedTextTag = ['em', 'b', 'i', 'u'];
this.spyWatched = $('body[data-target=".navspy"]');
},
/**
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
var $spyWatcheElement = this.$el.find('[data-id="portal_sidebar"]');
this._setElementId($spyWatcheElement);
// Nav Menu ScrollSpy
this._generateMenu();
return def;
},
//--------------------------------------------------------------------------
// Private
//---------------------------------------------------------------------------
/**
* create an unique id and added as a attribute of spyWatched element
*
* @private
* @param {string} prefix
* @param {Object} $el
*
*/
_setElementId: function (prefix, $el) {
var id = _.uniqueId(prefix);
this.spyWatched.find($el).attr('id', id);
return id;
},
/**
* generate the new spy menu
*
* @private
*
*/
_generateMenu: function () {
var self = this,
lastLI = false,
lastUL = null,
$bsSidenav = this.$el.find('.bs-sidenav');
$("#quote_content [id^=quote_header_], #quote_content [id^=quote_]", this.spyWatched).attr("id", "");
_.each(this.spyWatched.find("#quote_content h2, #quote_content h3"), function (el) {
var id, text;
switch (el.tagName.toLowerCase()) {
case "h2":
id = self._setElementId('quote_header_', el);
text = self._extractText($(el));
if (!text) {
break;
}
lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
lastUL = false;
break;
case "h3":
id = self._setElementId('quote_', el);
text = self._extractText($(el));
if (!text) {
break;
}
if (lastLI) {
if (!lastUL) {
lastUL = $("<ul class='nav flex-column'>").appendTo(lastLI);
}
$("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo(lastUL);
}
break;
}
el.setAttribute('data-anchor', true);
});
this.trigger_up('widgets_start_request', {$target: $bsSidenav});
},
/**
* extract text of menu title for sidebar
*
* @private
* @param {Object} $node
*
*/
_extractText: function ($node) {
var self = this;
var rawText = [];
_.each($node.contents(), function (el) {
var current = $(el);
if ($.trim(current.text())) {
var tagName = current.prop("tagName");
if (_.isUndefined(tagName) || (!_.isUndefined(tagName) && _.contains(self.authorizedTextTag, tagName.toLowerCase()))) {
rawText.push($.trim(current.text()));
}
}
});
return rawText.join(' ');
},
});
});

View file

@ -0,0 +1,41 @@
odoo.define('purchase.ToasterButton', function (require) {
'use strict';
const widgetRegistry = require('web.widget_registry');
const Widget = require('web.Widget');
const ToasterButton = Widget.extend({
template: 'purchase.ToasterButton',
events: Object.assign({}, Widget.prototype.events, {
'click .fa-info-circle': '_onClickButton',
}),
init: function (parent, data, node) {
this._super(...arguments);
this.button_name = node.attrs.button_name;
this.title = node.attrs.title;
this.id = data.res_id;
this.model = data.model;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onClickButton: function (ev) {
this._rpc({
method: this.button_name,
model: this.model,
args: [[this.id]],
}).then(res => {
if (res) {
this.displayNotification({ message: res.toast_message });
}
})
},
});
widgetRegistry.add('toaster_button', ToasterButton);
return ToasterButton;
});

View file

@ -0,0 +1,128 @@
odoo.define('purchase.purchase_steps', function (require) {
"use strict";
var core = require('web.core');
var PurchaseAdditionalTourSteps = core.Class.extend({
_get_purchase_stock_steps: function () {
return [
{
auto: true, // Useless final step to trigger congratulation message
trigger: ".o_purchase_order",
},
];
},
});
return PurchaseAdditionalTourSteps;
});
odoo.define('purchase.tour', function(require) {
"use strict";
var core = require('web.core');
var tour = require('web_tour.tour');
var _t = core._t;
var PurchaseAdditionalTourSteps = require('purchase.purchase_steps');
tour.register('purchase_tour' , {
url: "/web",
sequence: 40,
}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="purchase.menu_purchase_root"]',
content: _t("Let's try the Purchase app to manage the flow from purchase to reception and invoice control."),
position: 'right',
edition: 'community'
}, {
trigger: '.o_app[data-menu-xmlid="purchase.menu_purchase_root"]',
content: _t("Let's try the Purchase app to manage the flow from purchase to reception and invoice control."),
position: 'bottom',
edition: 'enterprise'
}, {
trigger: ".o_list_button_add",
extra_trigger: ".o_purchase_order",
content: _t("Let's create your first request for quotation."),
position: "bottom",
}, {
trigger: ".o_form_editable .o_field_res_partner_many2one[name='partner_id']",
extra_trigger: ".o_purchase_order",
content: _t("Search a vendor name, or create one on the fly."),
position: "bottom",
run: function (actions) {
actions.text("Azure Interior", this.$anchor.find("input"));
},
}, {
trigger: ".ui-menu-item > a:contains('Azure Interior')",
auto: true,
in_modal: false,
}, {
trigger: ".o_field_x2many_list_row_add > a",
content: _t("Add some products or services to your quotation."),
position: "bottom",
}, {
trigger: ".o_field_widget[name=product_id], .o_field_widget[name=product_template_id]",
extra_trigger: ".o_purchase_order",
content: _t("Select a product, or create a new one on the fly."),
position: "right",
run: function (actions) {
var $input = this.$anchor.find('input');
actions.text("DESK0001", $input.length === 0 ? this.$anchor : $input);
// fake keydown to trigger search
var keyDownEvent = jQuery.Event("keydown");
keyDownEvent.which = 42;
this.$anchor.trigger(keyDownEvent);
},
}, {
trigger: "a:contains('DESK0001')",
auto: true,
}, {
trigger: "td[name='name'][data-tooltip*='DESK0001']",
auto: true,
run: function () {} // wait for product creation
}, {
trigger: "div.o_field_widget[name='product_qty'] input ",
extra_trigger: ".o_purchase_order",
content: _t("Indicate the product quantity you want to order."),
position: "right",
run: 'text 12.0'
},
...tour.stepUtils.statusbarButtonsSteps('Send by Email', _t("Send the request for quotation to your vendor."), ".o_statusbar_buttons button[name='action_rfq_send']"),
{
trigger: ".modal-content",
auto: true,
run: function(actions){
// Check in case user must add email to vendor
var $input = $(".modal-content input[name='email']");
if ($input.length) {
actions.text("agrolait@example.com", $input);
actions.click($(".modal-footer button"));
}
}
}, {
trigger: ".modal-footer button[name='action_send_mail']",
extra_trigger: ".modal-footer button[name='action_send_mail']",
content: _t("Send the request for quotation to your vendor."),
position: "left",
run: 'click',
}, {
content: "Select price",
trigger: 'tbody tr.o_data_row .o_list_number[name="price_unit"]',
}, {
trigger: 'tbody tr.o_data_row .o_list_number[name="price_unit"] input',
extra_trigger: ".o_purchase_order",
content: _t("Once you get the price from the vendor, you can complete the purchase order with the right price."),
position: "right",
run: 'text 200.00'
}, {
auto: true,
trigger: ".o_purchase_order",
run: 'click',
}, ...tour.stepUtils.statusbarButtonsSteps('Confirm Order', _t("Confirm your purchase.")),
...new PurchaseAdditionalTourSteps()._get_purchase_stock_steps(),
]);
});

View file

@ -0,0 +1,27 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
const { Component } = owl;
class ButtonWithNotification extends Component {
setup() {
this.orm = useService("orm");
this.notification = useService("notification");
}
async onClick() {
const result = await this.orm.call(this.props.record.resModel, this.props.method, [this.props.record.resId]);
const message = result.toast_message;
this.notification.add(message, { type: "success" });
}
}
ButtonWithNotification.template = "purchase.ButtonWithNotification";
ButtonWithNotification.extractProps = ({ attrs }) => {
return {
method: attrs.button_name,
title: attrs.title,
};
};
registry.category("view_widgets").add("toaster_button", ButtonWithNotification);

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<t t-name="purchase.ButtonWithNotification" owl="1">
<button t-on-click="onClick" t-att-name="props.method" type="button" class="btn oe_inline btn-link" t-att-title="props.title">
<i class="fa fa-fw o_button_icon fa-info-circle"></i>
</button>
</t>
</template>

View file

@ -0,0 +1,34 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
const { Component, onWillStart } = owl;
export class PurchaseDashBoard extends Component {
setup() {
this.orm = useService("orm");
this.action = useService("action");
onWillStart(async () => {
this.purchaseData = await this.orm.call(
"purchase.order",
"retrieve_dashboard",
);
});
}
/**
* This method clears the current search query and activates
* the filters found in `filter_name` attibute from button pressed
*/
setSearchContext(ev) {
let filter_name = ev.currentTarget.getAttribute("filter_name");
let filters = filter_name.split(',');
let searchItems = this.env.searchModel.getSearchItems((item) => filters.includes(item.name));
this.env.searchModel.query = [];
for (const item of searchItems){
this.env.searchModel.toggleSearchItem(item.id);
}
}
}
PurchaseDashBoard.template = 'purchase.PurchaseDashboard'

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="purchase.PurchaseDashboard" owl="1">
<div class="o_purchase_dashboard container-fluid py-4 border-bottom bg-white">
<div class="row justify-content-between gap-3 gap-lg-0">
<div class="col-12 col-lg-5 col-xl-5 col-xxl-4 flex-grow-1 flex-lg-grow-0 flex-shrink-0">
<div class="grid gap-4">
<div class="g-col-3 g-col-sm-2 d-flex align-items-center py-2 justify-content-end text-end justify-content-lg-start text-lg-start text-break">
All RFQs
</div>
<div class="g-col-9 g-col-sm-10 grid gap-1">
<div class="g-col-4 p-0" t-on-click="setSearchContext" title="All Draft RFQs" filter_name="draft_rfqs">
<a href="#" class="btn btn-primary w-100 h-100 border-0 rounded-0 text-capitalize text-break fw-normal">
<div class="fs-2" t-out="purchaseData['all_to_send']"/>To Send
</a>
</div>
<div class="g-col-4 p-0" t-on-click="setSearchContext" title="All Waiting RFQs" filter_name="waiting_rfqs">
<a href="#" class="btn btn-primary w-100 h-100 border-0 rounded-0 text-capitalize text-break fw-normal">
<div class="fs-2" t-out="purchaseData['all_waiting']"/>Waiting
</a>
</div>
<div class="g-col-4 p-0" t-on-click="setSearchContext" title="All Late RFQs" filter_name="late_rfqs">
<a href="#" class="btn btn-primary w-100 h-100 border-0 rounded-0 text-capitalize text-break fw-normal">
<div class="fs-2" t-out="purchaseData['all_late']"/>Late
</a>
</div>
</div>
</div>
<div class="grid gap-4">
<div class="g-col-3 g-col-sm-2 d-flex align-items-center py-2 justify-content-end text-end justify-content-lg-start text-lg-start text-break">
My RFQs
</div>
<div class="g-col-9 g-col-sm-10 grid gap-2">
<div class="g-col-4 p-0" t-on-click="setSearchContext" title="My Draft RFQs" filter_name="draft_rfqs,my_purchases">
<a href="#" class="btn btn-light d-flex align-items-center w-100 h-100 p-0 border-0 bg-100 fw-normal">
<div class="w-100 p-2" t-out="purchaseData['my_to_send']"/>
</a>
</div>
<div class="g-col-4 p-0" t-on-click="setSearchContext" title="My Waiting RFQs" filter_name="waiting_rfqs,my_purchases">
<a href="#" class="btn btn-light d-flex align-items-center w-100 h-100 p-0 border-0 bg-100 fw-normal">
<div class="w-100 p-2" t-out="purchaseData['my_waiting']"/>
</a>
</div>
<div class="g-col-4 p-0" t-on-click="setSearchContext" title="My Late RFQs" filter_name="late_rfqs,my_purchases">
<a href="#" class="btn btn-light d-flex align-items-center w-100 h-100 p-0 border-0 bg-100 fw-normal">
<div class="w-100 p-2" t-out="purchaseData['my_late']"/>
</a>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-7 col-xl-6 col-xxl-5 flex-shrink-0">
<div class="d-flex flex-column justify-content-between gap-2 h-100">
<div class="grid gap-2 h-100">
<div class="g-col-6 g-col-md-6 grid gap-1 gap-md-4">
<div class="g-col-12 g-col-sm-4 g-col-lg-6 d-flex align-items-center justify-content-center text-center justify-content-md-end text-md-end mt-4 mt-sm-0 text-break">
Avg Order Value
</div>
<div class="g-col-12 g-col-sm-8 g-col-lg-5 d-flex align-items-center justify-content-center py-2 bg-light">
<span><t t-out="purchaseData['all_avg_order_value']"/></span>
</div>
</div>
<div class="g-col-6 g-col-md-6 grid gap-1 gap-md-4">
<div class="g-col-12 g-col-sm-4 g-col-lg-6 d-flex align-items-center py-2 justify-content-center text-center justify-content-md-end text-md-end mt-4 mt-sm-0 text-break">
Purchased Last 7 Days
</div>
<div class="g-col-12 g-col-sm-8 g-col-lg-6 d-flex align-items-center justify-content-center py-2 bg-light">
<span><t t-out="purchaseData['all_total_last_7_days']"/></span>
</div>
</div>
</div>
<div class="grid gap-2 h-100">
<div class="g-col-6 g-col-md-6 grid gap-1 gap-md-4">
<div class="g-col-12 g-col-sm-4 g-col-lg-6 d-flex align-items-center justify-content-center text-center justify-content-md-end text-md-end mt-4 mt-sm-0 text-break">
Lead Time to Purchase
</div>
<div class="g-col-12 g-col-sm-8 g-col-lg-5 d-flex align-items-center justify-content-center py-2 bg-light">
<span><t t-out="purchaseData['all_avg_days_to_purchase']"/> &#160;Days</span>
</div>
</div>
<div class="g-col-6 g-col-md-6 grid gap-1 gap-md-4">
<div class="g-col-12 g-col-md-4 g-col-sm-4 g-col-lg-6 d-flex align-items-center justify-content-center text-center justify-content-md-end text-md-end mt-4 mt-sm-0 text-break">
RFQs Sent Last 7 Days
</div>
<div class="g-col-12 g-col-sm-8 g-col-lg-6 d-flex align-items-center justify-content-center py-2 bg-light">
<span><t t-out="purchaseData['all_sent_rfqs']"/></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,19 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { kanbanView } from '@web/views/kanban/kanban_view';
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
import { PurchaseDashBoard } from '@purchase/views/purchase_dashboard';
export class PurchaseDashBoardKanbanRenderer extends KanbanRenderer {};
PurchaseDashBoardKanbanRenderer.template = 'purchase.PurchaseKanbanView';
PurchaseDashBoardKanbanRenderer.components= Object.assign({}, KanbanRenderer.components, {PurchaseDashBoard})
export const PurchaseDashBoardKanbanView = {
...kanbanView,
Renderer: PurchaseDashBoardKanbanRenderer,
};
registry.category("views").add("purchase_dashboard_kanban", PurchaseDashBoardKanbanView);

View file

@ -0,0 +1,7 @@
<templates>
<t t-name="purchase.PurchaseKanbanView" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<PurchaseDashBoard />
</xpath>
</t>
</templates>

View file

@ -0,0 +1,18 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { listView } from "@web/views/list/list_view";
import { ListRenderer } from "@web/views/list/list_renderer";
import { PurchaseDashBoard } from '@purchase/views/purchase_dashboard';
export class PurchaseDashBoardRenderer extends ListRenderer {};
PurchaseDashBoardRenderer.template = 'purchase.PurchaseListView';
PurchaseDashBoardRenderer.components= Object.assign({}, ListRenderer.components, {PurchaseDashBoard})
export const PurchaseDashBoardListView = {
...listView,
Renderer: PurchaseDashBoardRenderer,
};
registry.category("views").add("purchase_dashboard_list", PurchaseDashBoardListView);

View file

@ -0,0 +1,7 @@
<templates>
<t t-name="purchase.PurchaseListView" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
<PurchaseDashBoard />
</xpath>
</t>
</templates>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<template>
<t t-name="purchase.ToasterButton">
<button t-att-name="widget.button_name" type="button" class="btn oe_inline btn-link" t-att-title="widget.title">
<i class="fa fa-fw o_button_icon fa-info-circle"></i>
</button>
</t>
</template>