Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] last_ordered_products: improved sale and purchase by adding this module #654

Draft
wants to merge 6 commits into
base: 18.0
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions last_ordered_products/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
24 changes: 24 additions & 0 deletions last_ordered_products/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
'name': "Last Ordered Products",
'version': '1.0',
'depends': ['sale_management', 'purchase', 'stock'],
'author': "Parthav Chodvadiya (PPCH)",
'category': '',
'description': """
Show last ordered products for customers in sale order and for vendors in purchase order
""",
'data': [
'data/last_ordered_products_tour.xml',
'views/account_move_form.xml',
'views/sale_order_form.xml',
'views/purchase_order_form.xml',
'views/product_views.xml',
],
'assets': {
'web.assets_backend': [
'last_ordered_products/static/src/**/*.js',
'last_ordered_products/static/src/**/*.xml',
],
},
'license': 'LGPL-3',
}
8 changes: 8 additions & 0 deletions last_ordered_products/data/last_ordered_products_tour.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="last_ordered_products_tour" model="web_tour.tour">
<field name="name">last_ordered_products_tour</field>
<field name="sequence">1</field>
<field name="rainbow_man_message">Congrats, best of luck catching such big fish! :&#41;</field>
</record>
</odoo>
4 changes: 4 additions & 0 deletions last_ordered_products/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import product_product
from . import product_template
from . import sale_order
from . import sale_order_line
132 changes: 132 additions & 0 deletions last_ordered_products/models/product_product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from datetime import datetime
from odoo import api, fields, models
from odoo.osv import expression


class ProductProduct(models.Model):
_inherit = 'product.product'

last_order_time = fields.Datetime(compute='_compute_last_order_time')
last_date_str = fields.Char(compute='_compute_last_order_time')

@api.depends_context('order_id')
def _compute_last_order_time(self):
"""Compute the last order time for each product based on the latest sale or purchase."""

order_type = False
if self.env.context.get('active_model') == 'sale.order.line':
partner_id = self.env['sale.order'].browse(self.env.context.get('order_id')).partner_id.id
order_type = 'sale'
elif self.env.context.get('active_model') == 'purchase.order.line':
partner_id = self.env['purchase.order'].browse(self.env.context.get('order_id')).partner_id.id
order_type = 'purchase'
elif self.env.context.get('active_model') == 'account.journal':
active_id = self.env.context.get('active_id')
if active_id:
order_type = self.env['account.journal'].browse(active_id).type
partner_id = self.env.context.get('partner_id') or self.env.context.get('default_partner_id')
else:
partner_id = self.env.context.get('partner_id')
order_type = self.env.context.get('order_type')

if not partner_id:
for record in self:
record.last_order_time = False
record.last_date_str = False
return

last_ordered_products = {}

if order_type == 'sale':
last_ordered_products = self._get_last_sold_products(partner_id)
elif order_type == 'purchase':
last_ordered_products = self._get_last_purchased_products(partner_id)

for record in self:
last_date = last_ordered_products.get(record.id)

record.last_order_time = last_date if last_date else False
record.last_date_str = self.env['product.template']._get_time_ago_string(last_date) if last_date else False

def _get_last_sold_products(self, partner_id):
'''Fetch products last sold to the given customer'''

sale_order_lines = self.env['sale.order.line'].search([
('order_id.partner_id', '=', partner_id)
])

if not sale_order_lines:
return {}

invoices = self.env['account.move'].search([
('partner_id', '=', partner_id),
('invoice_origin', 'in', sale_order_lines.order_id.mapped('name'))
])

last_sale_ordered_products = {}
invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices}
for sol in sale_order_lines:
last_date = invoice_dates.get(sol.order_id.name)
if last_date:
product_id = sol.product_id.id
if product_id not in last_sale_ordered_products or last_date > last_sale_ordered_products[product_id]:
last_sale_ordered_products[product_id] = last_date

return last_sale_ordered_products

def _get_last_purchased_products(self, partner_id):
'''Fetch products last purchased to the given vendor'''

purchase_order_line = self.env['purchase.order.line'].search([
('order_id.partner_id', '=', partner_id)
])

if not purchase_order_line:
return {}

invoices = self.env['account.move'].search([
('partner_id', '=', partner_id),
('invoice_origin', 'in', purchase_order_line.order_id.mapped('name'))
])

last_purchased_order_products = {}
invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices}
for sol in purchase_order_line:
last_date = invoice_dates.get(sol.order_id.name)
if last_date:
product_id = sol.product_id.id
if product_id not in last_purchased_order_products or last_date > last_purchased_order_products[product_id]:
last_purchased_order_products[product_id] = last_date

return last_purchased_order_products

@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
'''Modify product dropdown in sale order line to show last sold date'''

domain = args or []
partner_id = self.env.context.get('partner_id')
order_type = self.env.context.get('order_type')
active_id = self.env.context.get('active_id')
if not order_type and active_id:
order_type = self.env['account.journal'].browse(active_id).type

if partner_id:
last_ordered_products = {}
if order_type == 'sale':
last_ordered_products = self._get_last_sold_products(partner_id)
elif order_type == 'purchase':
last_ordered_products = self._get_last_purchased_products(partner_id)

product_ids = list(last_ordered_products.keys())

products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit)
limit_rest = limit and limit - len(products)
if limit_rest is None or limit_rest > 0:
products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest)

products = sorted(products, key=lambda p: last_ordered_products.get(p.id, datetime.min), reverse=True)

return [(product.id, product.display_name, self.env['product.template']._get_time_ago_string(last_ordered_products.get(product.id, False))) for product in products]

return super().name_search(name, args, operator, limit)
111 changes: 111 additions & 0 deletions last_ordered_products/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from datetime import datetime
from odoo import api, fields, models
from odoo.osv import expression


class ProductTemplate(models.Model):
_inherit = 'product.template'

last_order_time = fields.Datetime(compute='_compute_last_order_time')
last_date_str = fields.Char(compute='_compute_last_order_time')

@api.depends_context('order_id')
def _compute_last_order_time(self):
"""Compute the last order time for each product based on the latest sale or purchase."""

partner_id = self.env.context.get('partner_id')
order_type = self.env.context.get('order_type')

if not partner_id:
for record in self:
record.last_order_time = False
record.last_date_str = False
return

last_ordered_products = {}

if order_type == 'sale':
last_ordered_products = self._get_last_sold_products(partner_id)

for record in self:
last_date = last_ordered_products.get(record.id)

record.last_order_time = last_date if last_date else False
record.last_date_str = self._get_time_ago_string(last_date) if last_date else False

def _get_last_sold_products(self, partner_id):
'''Fetch products last sold to the given customer'''

sale_order_lines = self.env['sale.order.line'].search([
('order_id.partner_id', '=', partner_id)
])

if not sale_order_lines:
return {}

invoices = self.env['account.move'].search([
('partner_id', '=', partner_id),
('invoice_origin', 'in', sale_order_lines.order_id.mapped('name'))
])

last_sale_ordered_products = {}
invoice_dates = {inv.invoice_origin: inv.create_date for inv in invoices}
for sol in sale_order_lines:
last_date = invoice_dates.get(sol.order_id.name)
if last_date:
product_id = sol.product_id.product_tmpl_id.id
if product_id not in last_sale_ordered_products or last_date > last_sale_ordered_products[product_id]:
last_sale_ordered_products[product_id] = last_date

return last_sale_ordered_products

def _get_time_ago_string(self, last_date):
'''Convert datetime to human-readable time difference (e.g., "1d", "4h", "4mo")'''

if not last_date:
return ""

now = fields.Datetime.now()
diff = now - last_date

if diff.days > 365:
return f"{diff.days // 365}y"
elif diff.days > 30:
return f"{diff.days // 30}mo"
elif diff.days > 0:
return f"{diff.days}d"
elif diff.seconds >= 3600:
return f"{diff.seconds // 3600}h"
elif diff.seconds >= 60:
return f"{diff.seconds // 60}m"
else:
return f"{diff.seconds}s"

@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
'''Modify product dropdown in sale order line to show last sold date'''

domain = args or []
partner_id = self.env.context.get('partner_id')
order_type = self.env.context.get('order_type')
active_id = self.env.context.get('active_id')
if not order_type and active_id:
order_type = self.env['account.journal'].browse(active_id).type

if partner_id:
last_ordered_products = {}
if order_type == 'sale':
last_ordered_products = self._get_last_sold_products(partner_id)

product_ids = list(last_ordered_products.keys())

products = self.search_fetch(expression.AND([domain, [('id', 'in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit)
limit_rest = limit and limit - len(products)
if limit_rest is None or limit_rest > 0:
products |= self.search_fetch(expression.AND([domain, [('id', 'not in', product_ids)], [("name", operator, name)]]), ['display_name'], limit=limit_rest)

products = sorted(products, key=lambda p: p.last_order_time if p.last_order_time else datetime.min, reverse=True)

return [(product.id, product.display_name, product.last_date_str if product.last_date_str else False) for product in products]

return super().name_search(name, args, operator, limit)
20 changes: 20 additions & 0 deletions last_ordered_products/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from odoo import models


class SaleOrder(models.Model):
_inherit = 'sale.order'

def _get_action_add_from_catalog_extra_context(self):
return {
**super()._get_action_add_from_catalog_extra_context(),
'display_uom': self.env.user.has_group('uom.group_uom'),
}

def _get_product_catalog_order_data(self, products, **kwargs):
res = super()._get_product_catalog_order_data(products, **kwargs)
for product in products:
res[product.id]['uom'] = {
'display_name': product.uom_id.display_name,
'id': product.uom_id.id,
}
return res
19 changes: 19 additions & 0 deletions last_ordered_products/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from odoo import models


class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'

def _get_product_catalog_lines_data(self, **kwargs):
res = super()._get_product_catalog_lines_data(**kwargs)
if len(self) == 1:
res['uom'] = {
'display_name': self.product_id.uom_id.display_name,
'id': self.product_id.uom_id.id,
}
if self.product_id.uom_id != self.product_uom:
res['sale_uom'] = {
'display_name': self.product_uom.display_name,
'id': self.product_uom.id,
}
return res
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="web.AutoComplete" t-inherit-mode="extension">
<xpath expr="//t[@t-esc='option.label']" position="replace">
<t t-if="option.time_str">
<div class="d-flex justify-content-between">
<span><t t-esc="option.label" /></span>
<span><t t-esc="option.time_str" /></span>
</div>
</t>
<t t-else="">
<span t-esc="option.label"/>
</t>
</xpath>
</t>
</templates>
129 changes: 129 additions & 0 deletions last_ordered_products/static/src/js/tours/last_ordered_products.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/** @odoo-module **/

import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_service/tour_utils";

registry.category("web_tour.tours").add('last_ordered_products_tour', {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
isActive: ["community"],
trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']",
content: _t("Lets create a beautiful quotation in a few clicks ."),
tooltipPosition: "right",
run: "click",
},
{
isActive: ["enterprise"],
trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']",
content: _t("Let’s create a beautiful quotation in a few clicks ."),
tooltipPosition: "bottom",
run: "click",
},
{
trigger: "button.o_list_button_add",
content: _t("Build your first quotation right here!"),
tooltipPosition: "bottom",
run: "click",
},
{
trigger: ".o_field_res_partner_many2one[name='partner_id'] input",
content: _t("Search a customer name ('Azure Interior'"),
tooltipPosition: "right",
run: "edit Azure",
},
{
trigger: ".o-autocomplete--dropdown-item > a:contains('Azure')",
content: "Select azure interior",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add a",
content: _t("Add a product"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: ".o_field_widget[name='product_id'], .o_field_widget[name='product_template_id']",
content: _t("Select a product"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: ".o_field_sol_product_many2one[name='product_id'] input, .o_field_sol_product_many2one[name='product_id'] input",
content: _t("Search a product (Large Cabinet)'"),
tooltipPosition: "top",
run: "edit Large Cabinet",
},
{
trigger: ".o-autocomplete--dropdown-item > a:contains('Cabinet')",
content: _t("Select Large Cabinet"),
run: "click",
},
{
trigger: ".o_form_button_save",
content: _t("Save Manually"),
run: "click",
},
{
trigger: "button[name=action_confirm]",
content: _t("Confirm Sale Order"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: "#create_invoice_percentage",
content: _t("Create Invoice"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: "#create_invoice_open",
content: _t("Create Draft"),
tooltipPosition: "bottom",
run: "click"
},
{
content: "Breadcrumb back to Quotations",
trigger: ".breadcrumb-item:contains('Quotations')",
run: "click",
},
{
trigger: ".o_list_button_add",
content: _t("Create New Sale Order"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: ".o_field_res_partner_many2one[name='partner_id'] input",
content: _t("Search a customer name ('Azure Interior'"),
tooltipPosition: "right",
run: "edit Azure",
},
{
trigger: ".o-autocomplete--dropdown-item > a:contains('Azure')",
content: "Select azure interior",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add a",
content: _t("Add a product"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: ".o_field_widget[name='product_id'], .o_field_widget[name='product_template_id']",
content: _t("Select a product"),
tooltipPosition: "bottom",
run: "click"
},
{
trigger: "div[name='product_id'] .o-autocomplete--dropdown-item > a:contains('[E-COM07]')",
content: _t("You can see here Product Large Cabinet which is invoiced few time ago to this customer"),
tooltipPosition: "right",
run: "click",
},
]
});
10 changes: 10 additions & 0 deletions last_ordered_products/static/src/product_catalog/kanban_model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ProductCatalogKanbanModel } from "@product/product_catalog/kanban_model";
import { patch } from "@web/core/utils/patch";

patch(ProductCatalogKanbanModel.prototype, {
async _loadData(params){
const result = await super._loadData(...arguments);
result.records.sort((a, b) => new Date(b.last_order_time) - new Date(a.last_order_time));
return result;
}
});
12 changes: 12 additions & 0 deletions last_ordered_products/static/src/product_catalog/kanban_record.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ProductCatalogKanbanRecord } from "@product/product_catalog/kanban_record";
import { ProductCatalogLastOrderOrderLine } from "./order_line/order_line";
import { patch } from "@web/core/utils/patch";

patch(ProductCatalogKanbanRecord.prototype, {
get orderLineComponent() {
if (this.env.orderResModel === "sale.order") {
return ProductCatalogLastOrderOrderLine;
}
return super.orderLineComponent;
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ProductCatalogOrderLine } from "@product/product_catalog/order_line/order_line";

export class ProductCatalogLastOrderOrderLine extends ProductCatalogOrderLine {
static template = "ProductCatalogLastOrderOrderLine";
static props = {
...ProductCatalogLastOrderOrderLine.props,
sale_uom: { type: Object, optional: true },
uom: Object,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="ProductCatalogLastOrderOrderLine"
t-inherit="product.ProductCatalogOrderLine"
t-inherit-mode="primary">
<xpath expr="//span[hasclass('o_product_catalog_price')]" position="attributes">
<attribute name="t-if">!this.env.displayUoM</attribute>
</xpath>
<xpath expr="//span[hasclass('o_product_catalog_price')]" position="after">
<span class="o_product_catalog_price" t-if="env.displayUoM">
<t t-out="price"/>
/ <span t-att-class="{'fw-bold text-primary': props.sale_uom}">
<t t-if="props.sale_uom" t-esc="props.sale_uom.display_name"/>
<t t-else="" t-esc="props.uom.display_name"/>
</span>
</span>
</xpath>
</t>
</templates>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ProductCatalogSaleOrderLine } from "@sale_stock/product_catalog/sale_order_line/sale_order_line";
import { patch } from "@web/core/utils/patch";

patch(ProductCatalogSaleOrderLine, {
template: "ProductCatalogLastOrderOrderLine",
props: {
...ProductCatalogSaleOrderLine.props,
deliveredQty: Number,
sale_uom: { type: Object, optional: true },
uom: Object,
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ProductLabelSectionAndNoteFieldAutocomplete } from "@account/components/product_label_section_and_note_field/product_label_section_and_note_field";
import { patch } from "@web/core/utils/patch";

patch(ProductLabelSectionAndNoteFieldAutocomplete.prototype, {
mapRecordToOption(result) {
let res = super.mapRecordToOption(result)
let time_str = result[2] ? result[2] : ""
res['time_str'] = time_str
return res
},
});
1 change: 1 addition & 0 deletions last_ordered_products/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_last_ordered_products
158 changes: 158 additions & 0 deletions last_ordered_products/tests/test_last_ordered_products.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from odoo.tests.common import TransactionCase
from odoo.tests import tagged


@tagged('post_install', '-at_install')
class LastOrderedProductsTestCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()

cls.test_product_1 = cls.env['product.product'].create({
'name': 'New_Product_1',
'purchase_method': 'purchase'
})
cls.test_product_2 = cls.env['product.product'].create({
'name': 'New_Product_2',
'purchase_method': 'purchase'
})
cls.test_product_3 = cls.env['product.product'].create({
'name': 'New_Product_3',
'purchase_method': 'purchase'
})
cls.test_product_4 = cls.env['product.product'].create({
'name': 'New_Product_4',
'purchase_method': 'purchase'
})

cls.test_partner_1 = cls.env['res.partner'].create({
'name': 'new_partner'
})

cls.test_journal_type_sale = cls.env['account.journal'].search([
('type', '=', 'sale')
], limit=1)

cls.test_journal_type_purchase = cls.env['account.journal'].search([
('type', '=', 'purchase')
], limit=1)

cls.test_sale_order_1 = cls.env['sale.order'].create({
'partner_id': cls.test_partner_1.id,
'order_line': [
(0, 0, {
'name': cls.test_product_1.name,
'product_id': cls.test_product_1.id,
'product_uom_qty': 1,
'product_uom': cls.test_product_1.uom_id.id,
'price_unit': cls.test_product_1.list_price,
})
],
})
cls.test_sale_order_1.action_confirm()
so_context = {
'active_model': 'sale.order',
'active_ids': [cls.test_sale_order_1.id],
'active_id': cls.test_sale_order_1.id,
'default_journal_id': cls.test_journal_type_sale.id,
}
payment_params = {
'advance_payment_method': 'percentage',
'amount': 50,
}
cls.test_downpayment = cls.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params)
cls.test_downpayment.create_invoices()
cls.test_invoice_1 = cls.env['account.move'].search([
('invoice_origin', '=', cls.test_sale_order_1.name)
])
cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2024-02-10', cls.test_invoice_1.id))

cls.test_sale_order_2 = cls.env['sale.order'].create({
'partner_id': cls.test_partner_1.id,
'order_line': [
(0, 0, {
'name': cls.test_product_2.name,
'product_id': cls.test_product_2.id,
'product_uom_qty': 1,
'product_uom': cls.test_product_2.uom_id.id,
'price_unit': cls.test_product_2.list_price,
})
],
})
cls.test_sale_order_2.action_confirm()
so_context = {
'active_model': 'sale.order',
'active_ids': [cls.test_sale_order_2.id],
'active_id': cls.test_sale_order_2.id,
'default_journal_id': cls.test_journal_type_sale.id,
}
payment_params = {
'advance_payment_method': 'percentage',
'amount': 50,
}
cls.test_downpayment = cls.env['sale.advance.payment.inv'].with_context(so_context).create(payment_params)
cls.test_downpayment.create_invoices()
cls.test_invoice_2 = cls.env['account.move'].search([
('invoice_origin', '=', cls.test_sale_order_2.name)
])
cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2023-02-10', cls.test_invoice_2.id))

cls.test_purchase_order_1 = cls.env['purchase.order'].create({
'partner_id': cls.test_partner_1.id,
'order_line': [
(0, 0, {
'name': cls.test_product_3.name,
'product_id': cls.test_product_3.id,
'product_uom_qty': 1,
'product_uom': cls.test_product_3.uom_id.id,
'price_unit': cls.test_product_3.list_price,
})
],
})
cls.test_purchase_order_1.button_confirm()
cls.test_purchase_order_1.action_view_picking()
cls.test_purchase_order_1.action_create_invoice()
cls.test_bill_1 = cls.env['account.move'].search([
('invoice_origin', '=', cls.test_purchase_order_1.name)
])
cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2024-02-10', cls.test_bill_1.id))

cls.test_purchase_order_2 = cls.env['purchase.order'].create({
'partner_id': cls.test_partner_1.id,
'order_line': [
(0, 0, {
'name': cls.test_product_4.name,
'product_id': cls.test_product_4.id,
'product_uom_qty': 1,
'product_uom': cls.test_product_4.uom_id.id,
'price_unit': cls.test_product_4.list_price,
})
],
})
cls.test_purchase_order_2.button_confirm()
cls.test_purchase_order_2.action_view_picking()
cls.test_purchase_order_2.action_create_invoice()
cls.test_bill_2 = cls.env['account.move'].search([
('invoice_origin', '=', cls.test_purchase_order_2.name)
])
cls.env.cr.execute(""" UPDATE account_move set create_date = '%s' WHERE id = '%s'""" % ('2023-02-10', cls.test_bill_2.id))

def test_product_variant_in_sale_order(self):
so_context = {
'partner_id': self.test_partner_1.id,
'order_type': 'sale'
}
res = self.env['product.product'].with_context(so_context).name_search()
res_ids = [r[0] for r in res]
self.assertEqual(self.test_product_1.id, res_ids[0])
self.assertEqual(self.test_product_2.id, res_ids[1])

def test_product_variant_in_purchase_order(self):
po_context = {
'partner_id': self.test_partner_1.id,
'order_type': 'purchase'
}
res = self.env['product.product'].with_context(po_context).name_search()
res_ids = [r[0] for r in res]
self.assertEqual(self.test_product_3.id, res_ids[0])
self.assertEqual(self.test_product_4.id, res_ids[1])
15 changes: 15 additions & 0 deletions last_ordered_products/views/account_move_form.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="account_move_view_last_product_sold" model="ir.ui.view">
<field name="name">account.move.view.last.product.sold</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<xpath expr="//list//field[@name='product_id']" position="attributes">
<attribute name="context">{
'partner_id': parent.partner_id,
}</attribute>
</xpath>
</field>
</record>
</odoo>
34 changes: 34 additions & 0 deletions last_ordered_products/views/product_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="product_view_kanban_catalog_last_order_products" model="ir.ui.view">
<field name="name">product.view.kanban.catalog.last.order.products</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_view_kanban_catalog"/>
<field name="arch" type="xml">
<xpath expr="//div[@t-if='record.default_code.value']" position="replace">
<div t-if="record.default_code.value">
<field name="default_code"/>
</div>
</xpath>

<xpath expr="//div[@name='o_kanban_qty_available']" position="replace">
<div t-if="record.is_storable.raw_value"
name="o_kanban_qty_available">
<field name="qty_available"/> On Hand
<field name="virtual_available" invisible="1"/>
<span t-if="record.virtual_available.raw_value - record.qty_available.raw_value >= 0" class="text-success">
<span>(+</span><t t-out="record.virtual_available.raw_value - record.qty_available.raw_value"/><span>)</span>
</span>
<span t-else="" class="text-danger">
<span>(</span><t t-out="record.virtual_available.raw_value - record.qty_available.raw_value"/><span>)</span>
</span>
</div>
</xpath>

<field name="product_template_attribute_value_ids" position="after">
<span invisible="not last_date_str"><field name="last_date_str"/> ago</span>
<span invisible="true"><field name="last_order_time"/></span>
</field>
</field>
</record>
</odoo>
18 changes: 18 additions & 0 deletions last_ordered_products/views/purchase_order_form.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="purchase_order_view_last_product_sold" model="ir.ui.view">
<field name="name">purchase.order.view.last.product.sold</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//list//field[@name='product_id']" position="attributes">
<attribute name="context">{
'partner_id': parent.partner_id,
'quantity': product_qty,
'company_id': parent.company_id,
'order_type': 'purchase'
}</attribute>
</xpath>
</field>
</record>
</odoo>
32 changes: 32 additions & 0 deletions last_ordered_products/views/sale_order_form.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="sale_order_view_last_product_sold" model="ir.ui.view">
<field name="name">sale.order.view.last.product.sold</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//list//field[@name='product_id']" position="attributes">
<attribute name="context">{
'partner_id': parent.partner_id,
'quantity': product_uom_qty,
'pricelist': parent.pricelist_id,
'uom': product_uom,
'company_id': parent.company_id,
'default_lst_price': price_unit,
'order_type': 'sale'
}</attribute>
</xpath>
<xpath expr="//list//field[@name='product_template_id']" position="attributes">
<attribute name="context">{
'partner_id': parent.partner_id,
'quantity': product_uom_qty,
'pricelist': parent.pricelist_id,
'uom': product_uom,
'company_id': parent.company_id,
'default_lst_price': price_unit,
'order_type': 'sale'
}</attribute>
</xpath>
</field>
</record>
</odoo>