Compare commits

...

29 Commits

Author SHA1 Message Date
clementmbr
c7172a5c7f [IMP] add bool is_created_by_warehouse on locations 2024-05-27 20:40:51 -03:00
clementmbr
5023839119 [ADD] bew module stock_location_simple 2024-05-27 19:57:29 -03:00
Alexis de Lattre
b252bdff34 Add module stock_picking_batch_usability 2024-04-26 17:48:51 +02:00
Alexis de Lattre
550704288d base_usability: add siren and siret in display address method 2024-04-26 10:32:53 +02:00
Alexis de Lattre
320cfff25f stock_usability: always show picking_type_id in picking form view 2024-04-25 15:15:55 +02:00
Alexis de Lattre
26a7a42e8c pos_usability: add pos_type_id on warehouse form view 2024-04-24 13:42:10 +02:00
Alexis de Lattre
3ca4553eb5 product_detailed_type_stock: default product type is 'product' instead of 'consu'
Same behavior as in stock_usability that switch default type to 'product' instead of 'consu'
2024-04-23 21:40:39 +02:00
Alexis de Lattre
309d466374 base_usability + account_usability: give access to acc_holder_name on res.partner.bank (hidden by default) 2024-04-23 21:07:27 +02:00
Florian
61f43e5d02 Merge pull request #209 from akretion/14-adapt-update-entry
[IMP] account_invoice_update_wizard : Adapt wizard view and button to make it usable on accountring entries + small improvements
2024-04-17 11:55:52 +02:00
Alexis de Lattre
6d847dcbe9 mrp_usability: improve mrp reporting
Add product category in reporting
Pivot view by default instead of graph view
Default measure total qty instead of count
2024-04-08 10:34:37 +02:00
Alexis de Lattre
85b4cc25eb sale_down_payment: add ACL 2024-04-05 09:42:54 +02:00
Alexis de Lattre
901a0e5816 mrp_usability: Allow to change the destination location until 'Mark as done'
Native behavior: it is only possible to change the destination stock
location of a production order in draft state.
2024-04-04 16:50:52 +02:00
Alexis de Lattre
569b3fea1a sale_usability: improve tree view of sale.order 2024-04-04 13:51:32 +02:00
Alexis de Lattre
60589b1743 sale_purchase_no_product_template_menu: update entries in account 2024-04-03 17:21:00 +02:00
Alexis de Lattre
5ce7ed3fe7 [IMP] base_usability: improve report header methods on res.company 2024-03-27 11:18:11 +01:00
Alexis de Lattre
ab3562a737 [MIG] mrp_average_cost to v14 2024-03-24 16:08:01 +01:00
Alexis de Lattre
28c6aca721 [MIG] account_invoice_margin from v12 to v14 2024-03-21 18:25:30 +01:00
Alexis de Lattre
b353bb14a5 [MIG] sale_down_payment to v14 2024-03-15 15:23:43 +01:00
Alexis de Lattre
282e7142db sale_usability: track changes in delivered_qty in the chatter
This feature is native in the purchase module in v14. I consider that it
replaces the 3 modules service_line_qty_update_base/service_line_qty_update_purchase/service_line_qty_update_sale
2024-03-14 18:07:52 +01:00
Alexis de Lattre
763928c286 [MIG] rp_no_product_template_menu to v14
Add comment in stock_no_product_template_menu
2024-03-14 17:29:39 +01:00
Alexis de Lattre
4774b879fa [MIG] stock_no_product_template_menu to v14 2024-03-14 17:21:36 +01:00
Alexis de Lattre
18a5c22160 sale_margin_no_onchange: fix digits in field declaration 2024-03-14 17:19:20 +01:00
Alexis de Lattre
8f87df3f3d sale_margin_no_onchange: improve perf by using read_group
Cleanup code
2024-03-14 17:09:53 +01:00
matthieu.saison
6af447974c migration to 14.0 2024-03-14 17:09:53 +01:00
Florian da Costa
46f2e0e01d [IMP] Add bill date (for supplier invoice) and rename bill ref to supplier bill ref to be consistent with name in invoice 2024-03-11 12:29:35 +01:00
Alexis de Lattre
ad8edd00d2 sale_order_route: add route_id in sale.report 2024-02-27 12:51:22 +01:00
Alexis de Lattre
97d57e40eb [FIX] stock_usability: stop adding a field in stock_usability which is defined in mrp ! 2024-02-13 17:00:08 +01:00
Alexis de Lattre
fcf67f4fd9 [MIG] patch for reserved_qty and free_qty from v12 to v14 2024-02-13 16:17:56 +01:00
Florian da Costa
89f81053cd [IMP] account_invoice_update_wizard : rename button for accounting entries + hide invoice specific fields in case of accountring entries 2024-02-12 11:51:15 +01:00
99 changed files with 1588 additions and 1310 deletions

View File

@@ -1,2 +1 @@
from . import account_invoice from . import models
from . import account_invoice_report

View File

@@ -4,7 +4,7 @@
{ {
'name': 'Account Invoice Margin', 'name': 'Account Invoice Margin',
'version': '12.0.1.0.0', 'version': '14.0.1.0.0',
'category': 'Invoicing Management', 'category': 'Invoicing Management',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': 'Copy standard price on invoice line and compute margins', 'summary': 'Copy standard price on invoice line and compute margins',
@@ -15,10 +15,10 @@ This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>. <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com', 'website': 'https://github.com/akretion/odoo-usability',
'depends': ['account'], 'depends': ['account'],
'data': [ 'data': [
'account_invoice_view.xml', 'views/account_move.xml',
], ],
'installable': False, 'installable': True,
} }

View File

@@ -1,152 +0,0 @@
# Copyright 2015-2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
import odoo.addons.decimal_precision as dp
class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
standard_price_company_currency = fields.Float(
string='Cost Price in Company Currency', readonly=True,
digits=dp.get_precision('Product Price'),
help="Cost price in company currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
standard_price_invoice_currency = fields.Float(
string='Cost Price in Invoice Currency', readonly=True,
compute='_compute_margin', store=True,
digits=dp.get_precision('Product Price'),
help="Cost price in invoice currency in the unit of measure "
"of the invoice line")
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency', readonly=True, store=True,
compute='_compute_margin', currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency', readonly=True, store=True,
compute='_compute_margin', currency_field='company_currency_id')
margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True,
compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price")
@api.depends(
'standard_price_company_currency', 'invoice_id.currency_id',
'invoice_id.type', 'invoice_id.company_id',
'invoice_id.date_invoice', 'quantity', 'price_subtotal')
def _compute_margin(self):
for il in self:
standard_price_inv_cur = 0.0
margin_inv_cur = 0.0
margin_comp_cur = 0.0
margin_rate = 0.0
inv = il.invoice_id
if inv and inv.type in ('out_invoice', 'out_refund'):
# it works in _get_current_rate
# even if we set date = False in context
# standard_price_inv_cur is in the UoM of the invoice line
date = inv._get_currency_rate_date() or\
fields.Date.context_today(self)
company = inv.company_id
company_currency = company.currency_id
standard_price_inv_cur =\
company_currency._convert(
il.standard_price_company_currency,
inv.currency_id, company, date)
margin_inv_cur =\
il.price_subtotal - il.quantity * standard_price_inv_cur
margin_comp_cur = inv.currency_id._convert(
margin_inv_cur, company_currency, company, date)
if il.price_subtotal:
margin_rate = 100 * margin_inv_cur / il.price_subtotal
# for a refund, margin should be negative
# but margin rate should stay positive
if inv.type == 'out_refund':
margin_inv_cur *= -1
margin_comp_cur *= -1
il.standard_price_invoice_currency = standard_price_inv_cur
il.margin_invoice_currency = margin_inv_cur
il.margin_company_currency = margin_comp_cur
il.margin_rate = margin_rate
# We want to copy standard_price on invoice line for customer
# invoice/refunds. We can't do that via on_change of product_id,
# because it is not always played when invoice is created from code
# => we inherit write/create
# We write standard_price_company_currency even on supplier invoice/refunds
# because we don't have access to the 'type' of the invoice
@api.model
def create(self, vals):
if vals.get('product_id'):
pp = self.env['product.product'].browse(vals['product_id'])
std_price = pp.standard_price
inv_uom_id = vals.get('uom_id')
if inv_uom_id and inv_uom_id != pp.uom_id.id:
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
vals['standard_price_company_currency'] = std_price
return super(AccountInvoiceLine, self).create(vals)
def write(self, vals):
if not vals:
vals = {}
if 'product_id' in vals or 'uom_id' in vals:
for il in self:
if 'product_id' in vals:
if vals.get('product_id'):
pp = self.env['product.product'].browse(
vals['product_id'])
else:
pp = False
else:
pp = il.product_id or False
# uom_id is NOT a required field
if 'uom_id' in vals:
if vals.get('uom_id'):
inv_uom = self.env['uom.uom'].browse(
vals['uom_id'])
else:
inv_uom = False
else:
inv_uom = il.uom_id or False
std_price = 0.0
if pp:
std_price = pp.standard_price
if inv_uom and inv_uom != pp.uom_id:
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
il.write({'standard_price_company_currency': std_price})
return super(AccountInvoiceLine, self).write(vals)
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency',
compute='_compute_margin', store=True, readonly=True,
currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency',
compute='_compute_margin', store=True, readonly=True,
currency_field='company_currency_id')
@api.depends(
'type',
'invoice_line_ids.margin_invoice_currency',
'invoice_line_ids.margin_company_currency')
def _compute_margin(self):
res = self.env['account.invoice.line'].read_group(
[('invoice_id', 'in', self.ids)],
['invoice_id', 'margin_invoice_currency',
'margin_company_currency'],
['invoice_id'])
for re in res:
if re['invoice_id']:
inv = self.browse(re['invoice_id'][0])
if inv.type in ('out_invoice', 'out_refund'):
inv.margin_invoice_currency = re['margin_invoice_currency']
inv.margin_company_currency = re['margin_company_currency']

View File

@@ -1,60 +0,0 @@
# Copyright 2018-2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
# why digits=0 ??? Why is it like that in the native "account" module
user_currency_margin = fields.Float(
string="Margin", compute='_compute_user_currency_margin', digits=0)
_depends = {
'account.invoice': [
'account_id', 'amount_total_company_signed',
'commercial_partner_id', 'company_id',
'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id',
'journal_id', 'number', 'partner_bank_id', 'partner_id',
'payment_term_id', 'residual', 'state', 'type', 'user_id',
],
'account.invoice.line': [
'account_id', 'invoice_id', 'price_subtotal', 'product_id',
'quantity', 'uom_id', 'account_analytic_id',
'margin_company_currency',
],
'product.product': ['product_tmpl_id'],
'product.template': ['categ_id'],
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
'res.currency.rate': ['currency_id', 'name'],
'res.partner': ['country_id'],
}
@api.depends('currency_id', 'date', 'margin')
def _compute_user_currency_margin(self):
user_currency = self.env.user.company_id.currency_id
currency_rate = self.env['res.currency.rate'].search([
('rate', '=', 1),
'|',
('company_id', '=', self.env.user.company_id.id),
('company_id', '=', False)], limit=1)
base_currency = currency_rate.currency_id
for record in self:
date = record.date or fields.Date.today()
company = record.company_id
record.user_currency_margin = base_currency._convert(
record.margin, user_currency, company, date)
# TODO check for refunds
def _sub_select(self):
select_str = super(AccountInvoiceReport, self)._sub_select()
select_str += ", SUM(ail.margin_company_currency) AS margin"
return select_str
def _select(self):
select_str = super(AccountInvoiceReport, self)._select()
select_str += ", sub.margin AS margin"
return select_str

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2015-2017 Akretion (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_invoice_line_form" model="ir.ui.view">
<field name="name">margin.account.invoice.line.form</field>
<field name="model">account.invoice.line</field>
<field name="inherit_id" ref="account.view_invoice_line_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='analytic_tag_ids']/.." position="inside">
<field name="standard_price_company_currency"
string="Cost Price in Comp. Cur."
groups="base.group_no_one"/>
<field name="standard_price_invoice_currency"
string="Cost Price in Inv. Cur."
groups="base.group_no_one"/>
<field name="margin_invoice_currency"
string="Margin in Inv. Cur."
groups="base.group_no_one"/>
<field name="margin_company_currency"
string="Margin in Comp. Cur."
groups="base.group_no_one"/>
<label for="margin_rate" groups="base.group_no_one"/>
<div name="margin_rate" groups="base.group_no_one">
<field name="margin_rate" class="oe_inline"/> %
</div>
</xpath>
</field>
</record>
<record id="invoice_form" model="ir.ui.view">
<field name="name">margin.account.invoice.form</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<field name="move_id" position="after">
<field name="margin_invoice_currency"
string="Margin in Inv. Cur." groups="base.group_no_one"/>
<field name="margin_company_currency"
string="Margin in Comp. Cur." groups="base.group_no_one"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,2 @@
from . import account_move
from . import account_invoice_report

View File

@@ -0,0 +1,36 @@
# Copyright 2018-2019 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountInvoiceReport(models.Model):
_inherit = 'account.invoice.report'
margin = fields.Float(string='Margin', readonly=True)
# added margin_company_currency on account.move.line
_depends = {
'account.move': [
'name', 'state', 'move_type', 'partner_id', 'invoice_user_id', 'fiscal_position_id',
'invoice_date', 'invoice_date_due', 'invoice_payment_term_id', 'partner_bank_id',
],
'account.move.line': [
'quantity', 'price_subtotal', 'amount_residual', 'balance', 'amount_currency',
'move_id', 'product_id', 'product_uom_id', 'account_id', 'analytic_account_id',
'journal_id', 'company_id', 'currency_id', 'partner_id',
'margin_company_currency',
],
'product.product': ['product_tmpl_id'],
'product.template': ['categ_id'],
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
'res.currency.rate': ['currency_id', 'name'],
'res.partner': ['country_id'],
}
@api.model
def _select(self):
select_str = super()._select()
select_str += ", line.margin_company_currency * currency_table.rate AS margin"
return select_str

View File

@@ -0,0 +1,155 @@
# Copyright 2015-2021 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
standard_price_company_currency = fields.Float(
string='Unit Cost Price in Company Currency', readonly=True,
digits='Product Price',
help="Unit Cost price in company currency in the unit of measure "
"of the invoice line (which may be different from the unit "
"of measure of the product).")
standard_price_invoice_currency = fields.Float(
string='Unit Cost Price in Invoice Currency',
compute='_compute_margin', store=True, digits='Product Price',
help="Unit Cost price in invoice currency in the unit of measure "
"of the invoice line.")
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency', store=True,
compute='_compute_margin', currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency', store=True,
compute='_compute_margin', currency_field='company_currency_id')
margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True,
compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price")
@api.depends(
'standard_price_company_currency', 'move_id.currency_id',
'move_id.move_type', 'move_id.company_id',
'move_id.invoice_date', 'quantity', 'price_subtotal')
def _compute_margin(self):
for ml in self:
standard_price_inv_cur = 0.0
margin_inv_cur = 0.0
margin_comp_cur = 0.0
margin_rate = 0.0
move = ml.move_id
if move.move_type and move.move_type in ('out_invoice', 'out_refund'):
# it works in _get_current_rate
# even if we set date = False in context
# standard_price_inv_cur is in the UoM of the invoice line
date = move.date or fields.Date.context_today(self)
company = move.company_id
company_currency = company.currency_id
standard_price_inv_cur =\
company_currency._convert(
ml.standard_price_company_currency,
ml.currency_id, company, date)
margin_inv_cur =\
ml.price_subtotal - ml.quantity * standard_price_inv_cur
margin_comp_cur = move.currency_id._convert(
margin_inv_cur, company_currency, company, date)
if ml.price_subtotal:
margin_rate = 100 * margin_inv_cur / ml.price_subtotal
# for a refund, margin should be negative
# but margin rate should stay positive
if move.move_type == 'out_refund':
margin_inv_cur *= -1
margin_comp_cur *= -1
ml.standard_price_invoice_currency = standard_price_inv_cur
ml.margin_invoice_currency = margin_inv_cur
ml.margin_company_currency = margin_comp_cur
ml.margin_rate = margin_rate
# We want to copy standard_price on invoice line for customer
# invoice/refunds. We can't do that via on_change of product_id,
# because it is not always played when invoice is created from code
# => we inherit write/create
# We write standard_price_company_currency even on supplier invoice/refunds
# because we don't have access to the 'type' of the invoice
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('product_id') and not vals.get('display_type'):
pp = self.env['product.product'].browse(vals['product_id'])
std_price = pp.standard_price
inv_uom_id = vals.get('product_uom_id')
if inv_uom_id and inv_uom_id != pp.uom_id.id:
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
vals['standard_price_company_currency'] = std_price
return super().create(vals_list)
def write(self, vals):
if not vals:
vals = {}
if 'product_id' in vals or 'product_uom_id' in vals:
for il in self:
if 'product_id' in vals:
if vals.get('product_id'):
pp = self.env['product.product'].browse(
vals['product_id'])
else:
pp = False
else:
pp = il.product_id or False
# uom_id is NOT a required field
if 'product_uom_id' in vals:
if vals.get('product_uom_id'):
inv_uom = self.env['uom.uom'].browse(
vals['product_uom_id'])
else:
inv_uom = False
else:
inv_uom = il.uom_id or False
std_price = 0.0
if pp:
std_price = pp.standard_price
if inv_uom and inv_uom != pp.uom_id:
std_price = pp.uom_id._compute_price(
std_price, inv_uom)
il.write({'standard_price_company_currency': std_price})
return super().write(vals)
class AccountMove(models.Model):
_inherit = 'account.move'
margin_invoice_currency = fields.Monetary(
string='Margin in Invoice Currency',
compute='_compute_margin', store=True,
currency_field='currency_id')
margin_company_currency = fields.Monetary(
string='Margin in Company Currency',
compute='_compute_margin', store=True,
currency_field='company_currency_id')
@api.depends(
'move_type',
'invoice_line_ids.margin_invoice_currency',
'invoice_line_ids.margin_company_currency')
def _compute_margin(self):
rg_res = self.env['account.move.line'].read_group(
[
('move_id', 'in', self.ids),
('display_type', '=', False),
('exclude_from_invoice_tab', '=', False),
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
],
['move_id', 'margin_invoice_currency:sum', 'margin_company_currency:sum'],
['move_id'])
mapped_data = dict([(x['move_id'][0], {
'margin_invoice_currency': x['margin_invoice_currency'],
'margin_company_currency': x['margin_company_currency'],
}) for x in rg_res])
for move in self:
move.margin_invoice_currency = mapped_data.get(move.id, {}).get('margin_invoice_currency')
move.margin_company_currency = mapped_data.get(move.id, {}).get('margin_company_currency')

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015-2024 Akretion (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_form" model="ir.ui.view">
<field name="name">margin.account.move.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<group name="sale_info_group" position="inside">
<field name="margin_invoice_currency"
groups="base.group_no_one"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_company_currency"
groups="base.group_no_one"
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</group>
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='price_total']" position="after">
<field name="standard_price_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_rate" optional="hide" string="Margin Rate (%)" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
</xpath>
<xpath expr="//field[@name='invoice_line_ids']/form//field[@name='price_total']/.." position="inside">
<field name="standard_price_company_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="standard_price_invoice_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_invoice_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<field name="margin_company_currency"
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<label for="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
<div name="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}">
<field name="margin_rate" class="oe_inline"/> %
</div>
</xpath>
</field>
</record>
<record id="view_invoice_tree" model="ir.ui.view">
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree"/>
<field name="arch" type="xml">
<field name="amount_residual_signed" position="after">
<field name="margin_company_currency" optional="hide" sum="1" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund')" string="Margin"/>
</field>
</field>
</record>
</odoo>

View File

@@ -11,7 +11,8 @@
<field name="inherit_id" ref="account.view_move_form"/> <field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<button name="button_draft" position="before"> <button name="button_draft" position="before">
<button name="prepare_update_wizard" type="object" string="Update Invoice" states="posted" groups="account.group_account_invoice"/> <button name="prepare_update_wizard" type="object" string="Update Invoice" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', '=', 'entry')]}"/>
<button name="prepare_update_wizard" type="object" string="Update Entry" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', '!=', 'entry')]}"/>
</button> </button>
</field> </field>
</record> </record>

View File

@@ -21,6 +21,7 @@ class AccountMoveUpdate(models.TransientModel):
invoice_payment_term_id = fields.Many2one( invoice_payment_term_id = fields.Many2one(
'account.payment.term', string='Payment Term') 'account.payment.term', string='Payment Term')
ref = fields.Char(string='Reference') # field label is customized in the view ref = fields.Char(string='Reference') # field label is customized in the view
invoice_date = fields.Date()
invoice_origin = fields.Char(string='Source Document') invoice_origin = fields.Char(string='Source Document')
partner_bank_id = fields.Many2one( partner_bank_id = fields.Many2one(
'res.partner.bank', string='Bank Account') 'res.partner.bank', string='Bank Account')
@@ -30,7 +31,7 @@ class AccountMoveUpdate(models.TransientModel):
@api.model @api.model
def _simple_fields2update(self): def _simple_fields2update(self):
'''List boolean, date, datetime, char, text fields''' '''List boolean, date, datetime, char, text fields'''
return ['ref', 'invoice_origin'] return ['ref', 'invoice_origin', 'invoice_date']
@api.model @api.model
def _m2o_fields2update(self): def _m2o_fields2update(self):

View File

@@ -15,13 +15,15 @@
<field name="move_type" invisible="1"/> <field name="move_type" invisible="1"/>
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
<field name="partner_id" invisible="1"/> <field name="partner_id" invisible="1"/>
<field string="Bill Reference" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="ref"/> <field string="Bill Date" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="invoice_date"/>
<field string="Supplier Bill Reference" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="ref"/>
<field string="Customer Reference" attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}" name="ref"/> <field string="Customer Reference" attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}" name="ref"/>
<field name="invoice_origin"/> <field string="Ref" attrs="{'invisible': [('move_type', '!=', 'entry')]}" name="ref"/>
<field name="invoice_origin" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
<!-- update of payment term is broken --> <!-- update of payment term is broken -->
<!-- <field name="invoice_payment_term_id" widget="selection"/>--> <!-- <field name="invoice_payment_term_id" widget="selection"/>-->
<field name="partner_bank_id"/> <field name="partner_bank_id" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
<field name="user_id" options="{'no_open': True, 'no_create': True, 'no_create_edit': True}"/> <field name="user_id" options="{'no_open': True, 'no_create': True, 'no_create_edit': True}" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
</group> </group>
<group name="lines"> <group name="lines">
<field name="line_ids" nolabel="1" widget="section_and_note_one2many"> <field name="line_ids" nolabel="1" widget="section_and_note_one2many">
@@ -30,8 +32,8 @@
<field name="display_type" invisible="1"/> <field name="display_type" invisible="1"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
<field name="name"/> <field name="name"/>
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)]}"/> <field name="quantity" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)]}"/> <field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
<field name="analytic_account_id" attrs="{'invisible': [('display_type', '!=', False)]}" groups="analytic.group_analytic_accounting"/> <field name="analytic_account_id" attrs="{'invisible': [('display_type', '!=', False)]}" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" attrs="{'invisible': [('display_type', '!=', False)]}" groups="analytic.group_analytic_tags" widget="many2many_tags"/> <field name="analytic_tag_ids" attrs="{'invisible': [('display_type', '!=', False)]}" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
</tree> </tree>

View File

@@ -30,6 +30,7 @@
'views/product.xml', 'views/product.xml',
'views/res_config_settings.xml', 'views/res_config_settings.xml',
'views/res_company.xml', 'views/res_company.xml',
'views/res_partner.xml',
'views/account_report.xml', 'views/account_report.xml',
'views/account_reconcile_model.xml', 'views/account_reconcile_model.xml',
'wizard/account_invoice_mark_sent_view.xml', 'wizard/account_invoice_mark_sent_view.xml',

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_partner_property_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='bank_ids']/tree/field[@name='acc_holder_name']" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="optional">hide</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 14.0\n" "Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-07-01 10:02+0000\n" "POT-Creation-Date: 2024-03-26 21:27+0000\n"
"PO-Revision-Date: 2021-07-01 10:02+0000\n" "PO-Revision-Date: 2024-03-26 21:27+0000\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -15,6 +15,18 @@ msgstr ""
"Content-Transfer-Encoding: \n" "Content-Transfer-Encoding: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "%s with a capital of"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "APE:"
msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner_bank #: model:ir.model,name:base_usability.model_res_partner_bank
msgid "Bank Accounts" msgid "Bank Accounts"
@@ -25,11 +37,22 @@ msgstr ""
msgid "Bank Name" msgid "Bank Name"
msgstr "" msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "Capital:"
msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_company #: model:ir.model,name:base_usability.model_res_company
msgid "Companies" msgid "Companies"
msgstr "" msgstr ""
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Company"
msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner #: model:ir.model,name:base_usability.model_res_partner
msgid "Contact" msgid "Contact"
@@ -48,6 +71,7 @@ msgid "Customer Number:"
msgstr "" msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__display_name #: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_model__display_name #: model:ir.model.fields,field_description:base_usability.field_ir_model__display_name
#: model:ir.model.fields,field_description:base_usability.field_res_company__display_name #: model:ir.model.fields,field_description:base_usability.field_res_company__display_name
@@ -65,12 +89,24 @@ msgstr ""
msgid "E-mail:" msgid "E-mail:"
msgstr "" msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "EORI:"
msgstr ""
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Field"
msgstr ""
#. module: base_usability #. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search #: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
msgid "Group By" msgid "Group By"
msgstr "" msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__id
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__id #: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__id
#: model:ir.model.fields,field_description:base_usability.field_ir_model__id #: model:ir.model.fields,field_description:base_usability.field_ir_model__id
#: model:ir.model.fields,field_description:base_usability.field_res_company__id #: model:ir.model.fields,field_description:base_usability.field_res_company__id
@@ -87,6 +123,7 @@ msgid "Installable"
msgstr "" msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server____last_update #: model:ir.model.fields,field_description:base_usability.field_ir_mail_server____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_model____last_update #: model:ir.model.fields,field_description:base_usability.field_ir_model____last_update
#: model:ir.model.fields,field_description:base_usability.field_res_company____last_update #: model:ir.model.fields,field_description:base_usability.field_res_company____last_update
@@ -139,6 +176,11 @@ msgstr ""
msgid "Partner Tags" msgid "Partner Tags"
msgstr "" msgstr ""
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__print_report_name
msgid "Printed Report Name"
msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref #: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
#: model:ir.model.fields,field_description:base_usability.field_res_users__ref #: model:ir.model.fields,field_description:base_usability.field_res_users__ref
@@ -146,8 +188,14 @@ msgid "Reference"
msgstr "" msgstr ""
#. module: base_usability #. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search #: model:ir.model,name:base_usability.model_ir_actions_report
msgid "Search Countries" msgid "Report Action"
msgstr ""
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "SIRET:"
msgstr "" msgstr ""
#. module: base_usability #. module: base_usability
@@ -169,6 +217,14 @@ msgstr ""
msgid "Tel:" msgid "Tel:"
msgstr "" msgstr ""
#. module: base_usability
#: model:ir.model.fields,help:base_usability.field_ir_actions_report__print_report_name
msgid ""
"This is the filename of the report going to download. Keep empty to not "
"change the report filename. You can use a python expression with the "
"'object' and 'time' variables."
msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_users #: model:ir.model,name:base_usability.model_res_users
msgid "Users" msgid "Users"

View File

@@ -6,15 +6,27 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 14.0\n" "Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-07-01 10:02+0000\n" "POT-Creation-Date: 2024-03-26 21:27+0000\n"
"PO-Revision-Date: 2021-07-01 12:15+0200\n" "PO-Revision-Date: 2024-03-26 21:27+0000\n"
"Last-Translator: Alexis de Lattre <alexis@via.ecp.fr>\n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n" "Content-Transfer-Encoding: \n"
"Plural-Forms: \n" "Plural-Forms: \n"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "%s with a capital of"
msgstr "%s au capital de"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "APE:"
msgstr "APE :"
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner_bank #: model:ir.model,name:base_usability.model_res_partner_bank
msgid "Bank Accounts" msgid "Bank Accounts"
@@ -25,11 +37,22 @@ msgstr "Comptes bancaires"
msgid "Bank Name" msgid "Bank Name"
msgstr "Nom de la banque" msgstr "Nom de la banque"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "Capital:"
msgstr "Capital : "
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_company #: model:ir.model,name:base_usability.model_res_company
msgid "Companies" msgid "Companies"
msgstr "Sociétés" msgstr "Sociétés"
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Company"
msgstr "Société"
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner #: model:ir.model,name:base_usability.model_res_partner
msgid "Contact" msgid "Contact"
@@ -42,11 +65,13 @@ msgstr "Devise"
#. module: base_usability #. module: base_usability
#: code:addons/base_usability/models/res_partner.py:0 #: code:addons/base_usability/models/res_partner.py:0
#: code:addons/base_usability/models/res_partner.py:0
#, python-format #, python-format
msgid "Customer Number:" msgid "Customer Number:"
msgstr "N° client :" msgstr "N° client :"
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__display_name #: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__display_name
#: model:ir.model.fields,field_description:base_usability.field_ir_model__display_name #: model:ir.model.fields,field_description:base_usability.field_ir_model__display_name
#: model:ir.model.fields,field_description:base_usability.field_res_company__display_name #: model:ir.model.fields,field_description:base_usability.field_res_company__display_name
@@ -64,12 +89,24 @@ msgstr "Nom affiché"
msgid "E-mail:" msgid "E-mail:"
msgstr "E-mail :" msgstr "E-mail :"
#. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "EORI:"
msgstr "EORI :"
#. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
msgid "Field"
msgstr "Champ"
#. module: base_usability #. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search #: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
msgid "Group By" msgid "Group By"
msgstr "Grouper par" msgstr "Grouper par"
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__id
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__id #: model:ir.model.fields,field_description:base_usability.field_ir_mail_server__id
#: model:ir.model.fields,field_description:base_usability.field_ir_model__id #: model:ir.model.fields,field_description:base_usability.field_ir_model__id
#: model:ir.model.fields,field_description:base_usability.field_res_company__id #: model:ir.model.fields,field_description:base_usability.field_res_company__id
@@ -78,7 +115,7 @@ msgstr "Grouper par"
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__id #: model:ir.model.fields,field_description:base_usability.field_res_partner_category__id
#: model:ir.model.fields,field_description:base_usability.field_res_users__id #: model:ir.model.fields,field_description:base_usability.field_res_users__id
msgid "ID" msgid "ID"
msgstr "ID" msgstr ""
#. module: base_usability #. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.view_module_filter #: model_terms:ir.ui.view,arch_db:base_usability.view_module_filter
@@ -86,6 +123,7 @@ msgid "Installable"
msgstr "Installable" msgstr "Installable"
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_mail_server____last_update #: model:ir.model.fields,field_description:base_usability.field_ir_mail_server____last_update
#: model:ir.model.fields,field_description:base_usability.field_ir_model____last_update #: model:ir.model.fields,field_description:base_usability.field_ir_model____last_update
#: model:ir.model.fields,field_description:base_usability.field_res_company____last_update #: model:ir.model.fields,field_description:base_usability.field_res_company____last_update
@@ -136,7 +174,12 @@ msgstr "Personne (utilisé pour cacher des entrées de menu natifs)"
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_partner_category #: model:ir.model,name:base_usability.model_res_partner_category
msgid "Partner Tags" msgid "Partner Tags"
msgstr "Étiquettes du partenaire" msgstr "Étiquettes contact"
#. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__print_report_name
msgid "Printed Report Name"
msgstr "Nom du rapport imprimé"
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref #: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
@@ -145,11 +188,18 @@ msgid "Reference"
msgstr "Référence" msgstr "Référence"
#. module: base_usability #. module: base_usability
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search #: model:ir.model,name:base_usability.model_ir_actions_report
msgid "Search Countries" msgid "Report Action"
msgstr "" msgstr ""
#. module: base_usability #. module: base_usability
#: code:addons/base_usability/models/res_company.py:0
#, python-format
msgid "SIRET:"
msgstr "SIRET :"
#. module: base_usability
#: code:addons/base_usability/models/res_partner.py:0
#: code:addons/base_usability/models/res_partner.py:0 #: code:addons/base_usability/models/res_partner.py:0
#, python-format #, python-format
msgid "Supplier Number:" msgid "Supplier Number:"
@@ -158,7 +208,7 @@ msgstr "N° fournisseur :"
#. module: base_usability #. module: base_usability
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__name #: model:ir.model.fields,field_description:base_usability.field_res_partner_category__name
msgid "Tag Name" msgid "Tag Name"
msgstr "Nom de l'étiquette" msgstr "Libellé de l'étiquette"
#. module: base_usability #. module: base_usability
#: code:addons/base_usability/models/res_company.py:0 #: code:addons/base_usability/models/res_company.py:0
@@ -167,6 +217,14 @@ msgstr "Nom de l'étiquette"
msgid "Tel:" msgid "Tel:"
msgstr "Tél :" msgstr "Tél :"
#. module: base_usability
#: model:ir.model.fields,help:base_usability.field_ir_actions_report__print_report_name
msgid ""
"This is the filename of the report going to download. Keep empty to not "
"change the report filename. You can use a python expression with the "
"'object' and 'time' variables."
msgstr ""
#. module: base_usability #. module: base_usability
#: model:ir.model,name:base_usability.model_res_users #: model:ir.model,name:base_usability.model_res_users
msgid "Users" msgid "Users"

View File

@@ -3,6 +3,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, _ from odoo import api, models, _
from odoo.tools.misc import format_amount
class ResCompany(models.Model): class ResCompany(models.Model):
@@ -39,32 +40,78 @@ class ResCompany(models.Model):
'value': self.phone, 'value': self.phone,
# http://www.fileformat.info/info/unicode/char/1f4de/index.htm # http://www.fileformat.info/info/unicode/char/1f4de/index.htm
'icon': '\U0001F4DE', 'icon': '\U0001F4DE',
'label': _('Tel:')}, 'label': _('Tel:'),
},
'email': { 'email': {
'value': self.email, 'value': self.email,
# http://www.fileformat.info/info/unicode/char/2709/index.htm # http://www.fileformat.info/info/unicode/char/2709/index.htm
'icon': '\u2709', 'icon': '\u2709',
'label': _('E-mail:')}, 'label': _('E-mail:'),
},
'website': { 'website': {
'value': self.website, 'value': self.website,
'icon': '\U0001f310', 'icon': '\U0001f310',
'label': _('Website:')}, 'label': _('Website:'),
},
'vat': { 'vat': {
'value': self.vat, 'value': self.vat,
'label': _('VAT:')}, 'label': _('VAT:'),
},
'ape': {
'value': hasattr(self, 'ape') and self.ape or False,
'label': _('APE:'),
},
'siret': {
'value': hasattr(self, 'siret') and self.siret or False,
'label': _('SIRET:'),
},
'siren': {
'value': hasattr(self, 'siren') and self.siren or False,
'label': _('SIREN:'),
},
'eori': {
'value': self._get_eori(),
'label': _('EORI:'),
},
'capital': {
# 'capital_amount' added by base_company_extension
'value': hasattr(self, 'capital_amount') and self.capital_amount and format_amount(self.env, self.capital_amount, self.currency_id) or False,
'label': _('Capital:'),
}
} }
# 'legal_type' added by base_company_extension
if hasattr(self, 'legal_type') and self.legal_type:
options['capital']['label'] = _('%s with a capital of') % self.legal_type
return options return options
def _get_eori(self):
eori = False
if self.partner_id.country_id.code == 'FR' and hasattr(self, 'siret') and self.siret:
# Currently migrating from EORI-SIRET to EORI-SIREN :
# https://www.pwcavocats.com/fr/ealertes/ealertes-france/2023/avril/reforme-numero-eori-siren-siret.html
# But, for the moment, we continue to use EORI-SIRET
eori = f'FR{self.siret}'
return eori
def _report_company_legal_name(self): def _report_company_legal_name(self):
'''Method inherited in the module base_company_extension''' '''Method inherited in the module base_company_extension'''
self.ensure_one() self.ensure_one()
return self.name return self.name
def _report_header_line_details(self):
"""This method is designed to be inherited"""
# I decided not to put email in the default header because only a few very small
# companies have a generic company email address
line_details = [['phone', 'website', 'capital'], ['vat', 'siret', 'eori', 'ape']]
return line_details
# for reports # for reports
def _display_report_header( def _display_report_header(
self, line_details=[['phone', 'website'], ['vat']], self, line_details=None, icon=True, line_separator=' - '):
icon=True, line_separator=' - '):
self.ensure_one() self.ensure_one()
if line_details is None:
line_details = self._report_header_line_details()
res = '' res = ''
address = self.partner_id._display_address(without_company=True) address = self.partner_id._display_address(without_company=True)
address = address.replace('\n', ' - ') address = address.replace('\n', ' - ')

View File

@@ -125,6 +125,20 @@ class ResPartner(models.Model):
'label': _('Supplier Number:'), 'label': _('Supplier Number:'),
}, },
} }
if hasattr(self, 'siren'):
options['siren'] = {
'value': self.siren,
'label': _("SIREN:"),
}
if hasattr(self, 'siret'):
if hasattr(self, 'siren'): # l10n_fr_siret is installed
siret = self.siren and self.nic and self.siret or False
else:
siret = self.siret
options['siret'] = {
'value': siret,
'label': _("SIRET:"),
}
res = [] res = []
for detail in details: for detail in details:
if options.get(detail) and options[detail]['value']: if options.get(detail) and options[detail]['value']:

View File

@@ -27,6 +27,17 @@
</field> </field>
</record> </record>
<record id="res_partner_view_form_private" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.res_partner_view_form_private"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='bank_ids']/tree/field[@name='acc_holder_name']" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="optional">hide</attribute>
</xpath>
</field>
</record>
<record id="view_partner_simple_form" model="ir.ui.view"> <record id="view_partner_simple_form" model="ir.ui.view">
<field name="name">base_usability.title.on.partner.simplified.form</field> <field name="name">base_usability.title.on.partner.simplified.form</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>

View File

@@ -16,6 +16,10 @@
<field name="bank_name" position="after"> <field name="bank_name" position="after">
<field name="bank_id"/> <field name="bank_id"/>
</field> </field>
<field name="acc_holder_name" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="optional">hide</attribute>
</field>
</field> </field>
</record> </record>
</odoo> </odoo>

View File

@@ -1,10 +1,10 @@
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com) # Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'MRP Average Cost', 'name': 'MRP Average Cost',
'version': '12.0.1.0.0', # WARNING: we'll probably not port this module to v14, because part of its feature is now provided by the module mrp_account 'version': '14.0.1.0.0',
'category': 'Manufactuing', 'category': 'Manufactuing',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': 'Update standard_price upon validation of a manufacturing order', 'summary': 'Update standard_price upon validation of a manufacturing order',
@@ -12,16 +12,16 @@
MRP Average Cost MRP Average Cost
================ ================
By default, the official stock module updates the standard_price of a product that has costing_method = 'average' when validating an incoming picking. But the official 'mrp' module doesn't do that when you validate a manufactuging order. I initially developped this module for Odoo 12.0, when the module mrp_account didn't exist, so Odoo didn't support the update of the standard cost of a manufactured product.
This module adds this feature : when you validate a manufacturing order of a product that has costing method = 'average', the standard_price of the product will be updated by taking into account the standard_price of each raw material and also a number of work hours defined on the BOM. In the mrp_account module, you must use workcenters to take the labor costs into account. This module aims at encoding theorical labor costs on the BOM and using it to compute the cost of the finished product.
Together with this module, I recommend the use of my module product_usability, available in the same branch, which contains a backport of the model product.price.history from v8 to v7. With this module, when you validate a manufacturing order of a product that has costing method = 'average', the standard_price of the product will be updated by taking into account the standard_price of each raw material and also a number of work hours defined on the BOM plus the extra cost defined of the BOM.
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>. This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com', 'website': 'https://github.com/akretion/odoo-usability',
'depends': ['mrp'], 'depends': ['mrp'],
'data': [ 'data': [
'security/mrp_average_cost_security.xml', 'security/mrp_average_cost_security.xml',
@@ -29,5 +29,5 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
'data/mrp_data.xml', 'data/mrp_data.xml',
'views/mrp_view.xml', 'views/mrp_view.xml',
], ],
'installable': False, 'installable': True,
} }

View File

@@ -1,11 +1,9 @@
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com) # Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _ from odoo import models, fields, api, _
import odoo.addons.decimal_precision as dp from odoo.tools import float_compare
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_is_zero
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -17,23 +15,19 @@ class MrpBomLabourLine(models.Model):
bom_id = fields.Many2one( bom_id = fields.Many2one(
comodel_name='mrp.bom', comodel_name='mrp.bom',
string='Labour Lines', string='Bill of Material',
ondelete='cascade') ondelete='cascade')
labour_time = fields.Float( labour_time = fields.Float(
string='Labour Time', string='Labour Time',
required=True, required=True,
digits=dp.get_precision('Labour Hours'), digits='Labour Hours',
help="Average labour time for the production of " help="Average labour time for the production of "
"items of the BOM, in hours.") "items of the BOM, in hours.")
labour_cost_profile_id = fields.Many2one( labour_cost_profile_id = fields.Many2one(
comodel_name='labour.cost.profile', comodel_name='labour.cost.profile',
string='Labour Cost Profile', string='Labour Cost Profile',
required=True) required=True)
note = fields.Text()
note = fields.Text(
string='Note')
_sql_constraints = [( _sql_constraints = [(
'labour_time_positive', 'labour_time_positive',
@@ -44,6 +38,26 @@ class MrpBomLabourLine(models.Model):
class MrpBom(models.Model): class MrpBom(models.Model):
_inherit = 'mrp.bom' _inherit = 'mrp.bom'
labour_line_ids = fields.One2many(
'mrp.bom.labour.line', 'bom_id', string='Labour Lines')
total_labour_cost = fields.Float(
compute='_compute_total_labour_cost', digits='Product Price', store=True)
extra_cost = fields.Float(
tracking=True, digits='Product Price',
help="Extra cost for the production of the quantity of "
"items of the BOM, in company currency. "
"You can use this field to enter the cost of the consumables "
"that are used to produce the product but are not listed in "
"the BOM")
total_components_cost = fields.Float(
compute='_compute_total_cost', digits='Product Price')
total_cost = fields.Float(
compute='_compute_total_cost', digits='Product Price',
help="Total cost for the quantity and unit of measure of the bill of material. "
"Total Cost = Total Components Cost + Total Labour Cost + Extra Cost")
company_currency_id = fields.Many2one(
related='company_id.currency_id', string='Company Currency')
@api.depends( @api.depends(
'labour_line_ids.labour_time', 'labour_line_ids.labour_time',
'labour_line_ids.labour_cost_profile_id.hour_cost') 'labour_line_ids.labour_cost_profile_id.hour_cost')
@@ -70,107 +84,77 @@ class MrpBom(models.Model):
bom.total_components_cost = comp_cost bom.total_components_cost = comp_cost
bom.total_cost = total_cost bom.total_cost = total_cost
labour_line_ids = fields.One2many(
'mrp.bom.labour.line', 'bom_id', string='Labour Lines')
total_labour_cost = fields.Float(
compute='_compute_total_labour_cost', readonly=True,
digits=dp.get_precision('Product Price'),
string="Total Labour Cost", store=True)
extra_cost = fields.Float(
string='Extra Cost', track_visibility='onchange',
digits=dp.get_precision('Product Price'),
help="Extra cost for the production of the quantity of "
"items of the BOM, in company currency. "
"You can use this field to enter the cost of the consumables "
"that are used to produce the product but are not listed in "
"the BOM")
total_components_cost = fields.Float(
compute='_compute_total_cost', readonly=True,
digits=dp.get_precision('Product Price'),
string='Total Components Cost')
total_cost = fields.Float(
compute='_compute_total_cost', readonly=True,
string='Total Cost',
digits=dp.get_precision('Product Price'),
help="Total Cost = Total Components Cost + "
"Total Labour Cost + Extra Cost")
company_currency_id = fields.Many2one(
related='company_id.currency_id', string='Company Currency')
@api.model @api.model
def _phantom_update_product_standard_price(self): def _phantom_update_product_standard_price(self):
logger.info('Start to auto-update cost price from phantom bom') logger.info('Start to auto-update cost price from phantom boms')
boms = self.search([('type', '=', 'phantom')]) boms = self.search([('type', '=', 'phantom')])
boms.with_context( boms.manual_update_product_standard_price()
product_price_history_origin='Automatic update of Phantom BOMs')\ logger.info('End of the auto-update cost price from phantom boms')
.manual_update_product_standard_price()
logger.info('End of the auto-update cost price from phantom bom')
return True
def manual_update_product_standard_price(self): def manual_update_product_standard_price(self):
if 'product_price_history_origin' not in self._context: prec = self.env['decimal.precision'].precision_get(
self = self.with_context(
product_price_history_origin='Manual update from BOM')
precision = self.env['decimal.precision'].precision_get(
'Product Price') 'Product Price')
for bom in self: for bom in self:
wproduct = bom.product_id if bom.product_id:
if not wproduct: products = bom.product_id
wproduct = bom.product_tmpl_id else:
if float_compare( products = bom.product_tmpl_id.product_variant_ids
wproduct.standard_price, bom.total_cost, for product in products:
precision_digits=precision): standard_price = product._compute_bom_price(bom)
wproduct.with_context().write( if float_compare(product.standard_price, standard_price, precision_digits=prec):
{'standard_price': bom.total_cost}) product.write({'standard_price': standard_price})
logger.info( logger.info(
'Cost price updated to %s on product %s', 'Cost price updated to %s on product %s',
bom.total_cost, wproduct.display_name) standard_price, product.display_name)
return True
class MrpBomLine(models.Model): class MrpBomLine(models.Model):
_inherit = 'mrp.bom.line' _inherit = 'mrp.bom.line'
standard_price = fields.Float( standard_price = fields.Float(related='product_id.standard_price')
related='product_id.standard_price',
readonly=True,
string='Standard Price') class ProductProduct(models.Model):
_inherit = 'product.product'
def _compute_bom_price(self, bom, boms_to_recompute=False):
# Native method of mrp_account
# WARNING dirty hack ; I hope it doesn't break too many things
self.ensure_one()
bom_cost_per_unit_in_product_uom = 0
qty_product_uom = bom.product_uom_id._compute_quantity(bom.product_qty, self.uom_id)
if qty_product_uom:
bom_cost_per_unit_in_product_uom = bom.total_cost / qty_product_uom
return bom_cost_per_unit_in_product_uom
class LabourCostProfile(models.Model): class LabourCostProfile(models.Model):
_name = 'labour.cost.profile' _name = 'labour.cost.profile'
_inherit = ['mail.thread'] _inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Labour Cost Profile' _description = 'Labour Cost Profile'
name = fields.Char( name = fields.Char(
string='Name',
required=True, required=True,
track_visibility='onchange') tracking=True)
hour_cost = fields.Float( hour_cost = fields.Float(
string='Cost per Hour', string='Cost per Hour',
required=True, required=True,
digits=dp.get_precision('Product Price'), digits='Product Price',
track_visibility='onchange', tracking=True,
help="Labour cost per hour per person in company currency") help="Labour cost per hour per person in company currency")
company_id = fields.Many2one( company_id = fields.Many2one(
comodel_name='res.company', comodel_name='res.company', required=True,
string='Company', default=lambda self: self.env.company)
required=True,
default=lambda self: self.env['res.company']._company_default_get())
company_currency_id = fields.Many2one( company_currency_id = fields.Many2one(
related='company_id.currency_id', related='company_id.currency_id', store=True, string='Company Currency')
readonly=True,
store=True,
string='Company Currency')
@api.depends('name', 'hour_cost', 'company_currency_id.symbol') @api.depends('name', 'hour_cost', 'company_currency_id.symbol')
def name_get(self): def name_get(self):
res = [] res = []
for record in self: for record in self:
res.append((record.id, u'%s (%s %s)' % ( res.append((record.id, '%s (%s %s)' % (
record.name, record.hour_cost, record.name, record.hour_cost,
record.company_currency_id.symbol))) record.company_currency_id.symbol)))
return res return res
@@ -179,93 +163,25 @@ class LabourCostProfile(models.Model):
class MrpProduction(models.Model): class MrpProduction(models.Model):
_inherit = 'mrp.production' _inherit = 'mrp.production'
unit_cost = fields.Float(
string='Unit Cost', readonly=True,
digits=dp.get_precision('Product Price'),
help="This cost per unit in the unit of measure of the product "
"in company currency takes into account "
"the cost of the raw materials and the labour cost defined on"
"the BOM.")
company_currency_id = fields.Many2one( company_currency_id = fields.Many2one(
related='company_id.currency_id', readonly=True, related='company_id.currency_id', string='Company Currency')
string='Company Currency') # extra_cost is per unit in the UoM of the mrp.production (product_uom_id)
extra_cost = fields.Float(
compute='_compute_extra_cost', store=True, readonly=False,
help="For a regular production order, it takes into account the labor cost "
"and the extra cost defined on the bill of material.")
def compute_order_unit_cost(self): # Strategy for v14 : we write labor costs and bom's extra cost on the native field extra_cost
self.ensure_one() # of mrp.production => it is automatically added by the code of mrp_account
mo_total_price = 0.0 # In the UoM of the M0
labor_cost_per_unit = 0.0 # In the UoM of the product
extra_cost_per_unit = 0.0 # In the UoM of the product
subcontract_cost_per_unit = 0.0
# I read the raw materials MO, not on BOM, in order to make
# it work with the "dynamic" BOMs (few raw material are auto-added
# on the fly on MO)
prec = self.env['decimal.precision'].precision_get(
'Product Unit of Measure')
for raw_smove in self.move_raw_ids:
# I don't filter on state, in order to make it work with
# partial productions
# For partial productions, mo.product_qty is not updated
# so we compute with fully qty and we compute with all raw
# materials (consumed or not), so it gives a good price
# per unit at the end
raw_price = raw_smove.product_id.standard_price
raw_material_cost = raw_price * raw_smove.product_qty
logger.info(
'MO %s product %s: raw_material_cost=%s',
self.name, raw_smove.product_id.display_name,
raw_material_cost)
mo_total_price += raw_material_cost
if self.bom_id:
bom = self.bom_id
# if not bom.total_labour_cost:
# raise orm.except_orm(
# _('Error:'),
# _("Total Labor Cost is 0 on bill of material '%s'.")
# % bom.name)
if float_is_zero(bom.product_qty, precision_digits=prec):
raise UserError(_(
"Missing Product Quantity on bill of material '%s'.")
% bom.display_name)
bom_qty_product_uom = bom.product_uom_id._compute_quantity(
bom.product_qty, bom.product_tmpl_id.uom_id)
assert bom_qty_product_uom > 0, 'BoM qty should be positive'
labor_cost_per_unit = bom.total_labour_cost / bom_qty_product_uom
extra_cost_per_unit = bom.extra_cost / bom_qty_product_uom
if bom.type == 'subcontract':
one_finished_move = self.env['stock.move'].search([
('production_id', '=', self.id),
('product_id', '=', self.product_id.id),
('move_dest_ids', '!=', False)], limit=1)
if one_finished_move:
subcontract_cost_per_unit = one_finished_move.move_dest_ids[0].price_unit
# mo_standard_price and labor_cost_per_unit are
# in the UoM of the product (not of the MO/BOM)
mo_qty_product_uom = self.product_uom_id._compute_quantity(
self.product_qty, self.product_id.uom_id)
assert mo_qty_product_uom > 0, 'MO qty should be positive'
mo_standard_price = mo_total_price / mo_qty_product_uom
logger.info(
'MO %s: labor_cost_per_unit=%s extra_cost_per_unit=%s '
'subcontract_cost_per_unit=%s',
self.name, labor_cost_per_unit, extra_cost_per_unit,
subcontract_cost_per_unit)
mo_standard_price += labor_cost_per_unit
mo_standard_price += extra_cost_per_unit
mo_standard_price += subcontract_cost_per_unit
return mo_standard_price
def post_inventory(self): @api.depends('bom_id', 'product_id')
'''This is the method where _action_done() is called on finished move def _compute_extra_cost(self):
So we write on 'price_unit' of the finished move and THEN we call for prod in self:
super() which will call _action_done() which itself calls bom = prod.bom_id
product_price_update_before_done()''' if bom and bom.type == 'normal':
for order in self: extra_cost_bom_qty_uom = bom.extra_cost + bom.total_labour_cost
if order.product_id.cost_method == 'average': extra_cost_per_unit_in_prod_uom = 0
unit_cost = order.compute_order_unit_cost() qty_prod_uom = bom.product_uom_id._compute_quantity(bom.product_qty, prod.product_uom_id)
order.write({'unit_cost': unit_cost}) if qty_prod_uom:
logger.info('MO %s: unit_cost=%s', order.name, unit_cost) extra_cost_per_unit_in_prod_uom = extra_cost_bom_qty_uom / qty_prod_uom
order.move_finished_ids.filtered( prod.extra_cost = extra_cost_per_unit_in_prod_uom
lambda x: x.product_id == order.product_id).write({
'price_unit': unit_cost})
return super(MrpProduction, self).post_inventory()

View File

@@ -4,7 +4,7 @@
<record id="labour_cost_profile_rule" model="ir.rule"> <record id="labour_cost_profile_rule" model="ir.rule">
<field name="name">Labour Cost Profile multi-company</field> <field name="name">Labour Cost Profile multi-company</field>
<field name="model_id" ref="model_labour_cost_profile"/> <field name="model_id" ref="model_labour_cost_profile"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field> <field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
</record> </record>
</odoo> </odoo>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright (C) 2016-2019 Akretion (http://www.akretion.com/) Copyright (C) 2016-2024 Akretion (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com> @author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
@@ -13,23 +13,25 @@
<field name="model">mrp.bom</field> <field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/> <field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="picking_type_id" position="after"> <xpath expr="//page[@name='miscellaneous']/group" position="inside">
<field name="total_components_cost" widget="monetary" <group name="costs">
options="{'currency_field': 'company_currency_id'}"/> <field name="total_components_cost" widget="monetary"
<field name="total_labour_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
options="{'currency_field': 'company_currency_id'}"/> <field name="total_labour_cost" widget="monetary"
<field name="extra_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
options="{'currency_field': 'company_currency_id'}"/> <field name="extra_cost" widget="monetary"
<label for="total_cost"/> options="{'currency_field': 'company_currency_id'}"/>
<div> <label for="total_cost"/>
<field name="total_cost" widget="monetary" <div>
options="{'currency_field': 'company_currency_id'}" <field name="total_cost" widget="monetary"
class="oe_inline"/> options="{'currency_field': 'company_currency_id'}"
<button type="object" name="manual_update_product_standard_price" class="oe_inline"/>
string="Update Cost Price of Product" class="oe_link"/> <button type="object" name="manual_update_product_standard_price"
</div> string="Update Cost Price of Product" class="oe_link"/>
<field name="company_currency_id" invisible="1"/> </div>
</field> <field name="company_currency_id" invisible="1"/>
</group>
</xpath>
<notebook position="inside"> <notebook position="inside">
<page string="Labour" name="labour_lines"> <page string="Labour" name="labour_lines">
<group name="labour_lines_grp"> <group name="labour_lines_grp">
@@ -117,10 +119,10 @@
<field name="inherit_id" ref="mrp.mrp_production_form_view"/> <field name="inherit_id" ref="mrp.mrp_production_form_view"/>
<field name="model">mrp.production</field> <field name="model">mrp.production</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="availability" position="after"> <xpath expr="//page[@name='miscellaneous']//field[@name='origin']/.." position="inside">
<field name="unit_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}" attrs="{'invisible': [('state', '!=', 'done')]}"/> <field name="extra_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
</field> </xpath>
</field> </field>
</record> </record>

View File

@@ -4,7 +4,7 @@
{ {
'name': 'MRP No Product Template Menu', 'name': 'MRP No Product Template Menu',
'version': '12.0.1.0.0', 'version': '14.0.1.0.0',
'category': 'Manufacturing', 'category': 'Manufacturing',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': "Replace product.template menu entries by product.product menu", 'summary': "Replace product.template menu entries by product.product menu",
@@ -22,9 +22,9 @@ This module has been written by Alexis de Lattre
from Akretion <alexis.delattre@akretion.com>. from Akretion <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com', 'website': 'https://github.com/akretion/odoo-usability',
'depends': ['mrp', 'sale_purchase_no_product_template_menu'], 'depends': ['mrp', 'sale_purchase_no_product_template_menu'],
'auto_install': True, 'auto_install': True,
'data': ['mrp_view.xml'], 'data': ['mrp_view.xml'],
'installable': False, 'installable': True,
} }

View File

@@ -1,21 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2016-2019 Akretion France (http://www.akretion.com/) Copyright 2016-2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com> @author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
<odoo> <odoo>
<record id="product_product_action_mrp" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form,kanban</field>
<field name="context">{'search_default_consumable': 1, 'default_type': 'product'}</field>
</record>
<record id="mrp.menu_mrp_product_form" model="ir.ui.menu"> <record id="mrp.menu_mrp_product_form" model="ir.ui.menu">
<field name="action" ref="product_product_action_mrp"/> <field name="action" ref="product.product_normal_action"/>
</record> </record>
<!-- we don't care about:
"search_default_consumable": 1 => not useful
"default_type": 'product' : stock_usability make it the default... no need to bother
-->
</odoo> </odoo>

View File

@@ -14,6 +14,7 @@
'data': [ 'data': [
'views/mrp_production.xml', 'views/mrp_production.xml',
'views/product_template.xml', 'views/product_template.xml',
'views/stock_move_line.xml',
# 'report/mrp_report.xml' # TODO # 'report/mrp_report.xml' # TODO
], ],
'installable': True, 'installable': True,

View File

@@ -3,12 +3,23 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models from odoo import api, fields, models
class MrpProduction(models.Model): class MrpProduction(models.Model):
_inherit = 'mrp.production' _inherit = 'mrp.production'
# Allow to change the destination location until 'Mark as done'.
# Native behavior: it is only possible to change it in draft state.
location_dest_id = fields.Many2one(states={
'draft': [('readonly', False)], # native
'confirmed': [('readonly', False)], # added
'progress': [('readonly', False)], # added
'to_close': [('readonly', False)], # added
}, tracking=True)
# Add field product_categ_id for reporting only
product_categ_id = fields.Many2one(related='product_id.categ_id', store=True)
# Method used by the report, inherited in this module # Method used by the report, inherited in this module
@api.model @api.model
def get_stock_move_sold_out_report(self, move): def get_stock_move_sold_out_report(self, move):

View File

@@ -16,8 +16,9 @@
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_src_id']" position="replace"/> <xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_src_id']" position="replace"/>
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_dest_id']" position="replace"/> <xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_dest_id']" position="replace"/>
<field name="bom_id" position="after"> <field name="bom_id" position="after">
<field name="location_src_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <!-- no need to set readonly via attrs, the readonly status is in the field definition -->
<field name="location_dest_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <field name="location_src_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" />
<field name="location_dest_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}"/>
</field> </field>
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='date_deadline']" position="after"> <xpath expr="//page[@name='miscellaneous']/group/group/field[@name='date_deadline']" position="after">
<field name="date_start"/> <field name="date_start"/>
@@ -40,4 +41,20 @@
</field> </field>
</record> </record>
<!-- Menu Manufacturing > Reporting > Manufacturing orders -->
<record id="mrp.mrp_production_report" model="ir.actions.act_window">
<!-- Change order: pivot first instead of graph -->
<field name="view_mode">pivot,graph,form</field>
</record>
<record id="view_production_pivot" model="ir.ui.view">
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.view_production_pivot"/>
<field name="arch" type="xml">
<pivot position="inside">
<field name="product_uom_qty" type="measure"/>
</pivot>
</field>
</record>
</odoo> </odoo>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_move_line_form" model="ir.ui.view">
<field name="name">mrp_usability.stock.move.line.form</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_move_line_form" />
<field name="arch" type="xml">
<field name="reference" position="before">
<field name="production_id" attrs="{'invisible': [('production_id', '=', False)]}"/>
</field>
</field>
</record>
</odoo>

View File

@@ -35,6 +35,7 @@ Akretion:
"views/pos_config.xml", "views/pos_config.xml",
"views/product.xml", "views/product.xml",
"views/pos_payment_method.xml", "views/pos_payment_method.xml",
"views/stock_warehouse.xml",
], ],
"installable": True, "installable": True,
} }

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_warehouse" model="ir.ui.view">
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse"/>
<field name="arch" type="xml">
<field name="out_type_id" position="after">
<field name="pos_type_id"/>
</field>
</field>
</record>
</odoo>

View File

@@ -10,4 +10,4 @@ class ProductTemplate(models.Model):
detailed_type = fields.Selection(selection_add=[ detailed_type = fields.Selection(selection_add=[
('product', 'Storable Product') ('product', 'Storable Product')
], ondelete={'product': 'set default'}) ], ondelete={'product': 'set default'}, default='product')

View File

@@ -1,10 +1,10 @@
# Copyright 2019 Akretion France (http://www.akretion.com) # Copyright 2019-2024 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{ {
'name': 'Sale Down Payment', 'name': 'Sale Down Payment',
'version': '12.0.1.0.0', 'version': '14.0.1.0.0',
'category': 'Sales', 'category': 'Sales',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': 'Link payment to sale orders', 'summary': 'Link payment to sale orders',
@@ -14,7 +14,7 @@ Sale Down Payment
This module adds a link between payments and sale orders. It allows to see down payments directly on the sale order form view. This module adds a link between payments and sale orders. It allows to see down payments directly on the sale order form view.
After processing a bank statement, you can start a wizard to link unreconciled incoming payments to a sale order. There is also a button *Register Payment* on the sale order. After processing a bank statement, you can start a wizard to link unreconciled incoming payments to a sale order (NOT ported to v14 so far). There is also a button *Register Payment* on the sale order.
This module targets B2B companies that don't want to generate a down payment invoice for an advanced payment. This module targets B2B companies that don't want to generate a down payment invoice for an advanced payment.
@@ -25,11 +25,13 @@ This module has been written by Alexis de Lattre from Akretion
'website': 'http://www.akretion.com', 'website': 'http://www.akretion.com',
'depends': ['sale'], 'depends': ['sale'],
'data': [ 'data': [
'wizard/account_bank_statement_sale_view.xml', 'security/ir.model.access.csv',
'views/account_bank_statement.xml', 'wizard/account_payment_register_sale_view.xml',
# 'wizard/account_bank_statement_sale_view.xml',
# 'views/account_bank_statement.xml',
'views/sale.xml', 'views/sale.xml',
'views/account_move_line.xml', 'views/account_move_line.xml',
'views/account_payment.xml', 'views/account_payment.xml',
], ],
'installable': False, 'installable': True,
} }

View File

@@ -1,3 +1,4 @@
from . import sale from . import sale
from . import account_move
from . import account_move_line from . import account_move_line
from . import account_payment from . import account_payment

View File

@@ -0,0 +1,26 @@
# Copyright 2023-2024 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def _post(self, soft=True):
res = super()._post(soft=soft)
amlo = self.env['account.move.line']
for move in self:
if move.state == 'posted' and move.move_type == 'out_invoice':
sales = move.invoice_line_ids.sale_line_ids.order_id
if sales:
mlines = amlo.search([('sale_id', 'in', sales.ids)])
if mlines:
mlines_to_reconcile = move.line_ids.filtered(
lambda line: line.account_id ==
move.commercial_partner_id.property_account_receivable_id)
mlines_to_reconcile |= mlines
mlines_to_reconcile.remove_move_reconcile()
mlines_to_reconcile.reconcile()
return res

View File

@@ -1,4 +1,4 @@
# Copyright 2019 Akretion France (http://www.akretion.com) # Copyright 2019-2024 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
@@ -9,24 +9,23 @@ from odoo.exceptions import ValidationError
class AccountMoveLine(models.Model): class AccountMoveLine(models.Model):
_inherit = 'account.move.line' _inherit = 'account.move.line'
sale_id = fields.Many2one('sale.order', string='Sale Order') sale_id = fields.Many2one(
account_internal_type = fields.Selection( 'sale.order', string='Sale Order', check_company=True,
related='account_id.user_type_id.type', store=True, domain="[('partner_invoice_id', 'child_of', partner_id), ('state', '!=', 'cancel'), ('invoice_status', '!=', 'invoiced'), ('company_id', '=', company_id)]")
string='Account Internal Type')
@api.constrains('sale_id', 'account_id') @api.constrains('sale_id', 'account_id')
def sale_id_check(self): def _sale_id_check(self):
for line in self: for line in self:
if line.sale_id and line.account_id.internal_type != 'receivable': if line.sale_id and line.account_internal_type != 'receivable':
raise ValidationError(_( raise ValidationError(_(
"The account move line '%s' is linked to sale order '%s' " "The account move line '%s' is linked to sale order '%s' "
"but it uses account '%s' which is not a receivable " "but it uses account '%s' which is not a receivable "
"account.") "account.")
% (line.name, % (line.display_name,
line.sale_id.name, line.sale_id.display_name,
line.account_id.display_name)) line.account_id.display_name))
@api.onchange('account_id') @api.onchange('account_id')
def sale_advance_payement_account_id_change(self): def sale_advance_payement_account_id_change(self):
if self.sale_id and self.account_id.user_type_id.type != 'receivable': if self.sale_id and self.account_internal_type != 'receivable':
self.sale_id = False self.sale_id = False

View File

@@ -1,9 +1,8 @@
# Copyright 2019 Akretion France (http://www.akretion.com) # Copyright 2019-2024 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models from odoo import fields, models
from odoo.tools import float_round
class AccountPayment(models.Model): class AccountPayment(models.Model):
@@ -11,49 +10,24 @@ class AccountPayment(models.Model):
sale_id = fields.Many2one('sale.order', string='Sale Order') sale_id = fields.Many2one('sale.order', string='Sale Order')
def action_validate_invoice_payment(self): def _prepare_move_line_default_vals(self, write_off_line_vals=None):
if self.sale_id: line_vals_list = super()._prepare_move_line_default_vals(
self.post() write_off_line_vals=write_off_line_vals)
else: # Add to the receivable/payable line
return super(AccountPayment, self).\
action_validate_invoice_payment()
def _get_counterpart_move_line_vals(self, invoice=False):
res = super(AccountPayment, self)._get_counterpart_move_line_vals(
invoice=invoice)
if self.sale_id:
res['sale_id'] = self.sale_id.id
return res
class AccountAbstractPayment(models.AbstractModel):
_inherit = "account.abstract.payment"
def default_get(self, fields_list):
res = super(AccountAbstractPayment, self).default_get(fields_list)
if ( if (
self._context.get('active_model') == 'sale.order' and self.sale_id and
self._context.get('active_id')): len(line_vals_list) >= 2 and
so = self.env['sale.order'].browse(self._context['active_id']) line_vals_list[1].get('account_id') == self.destination_account_id.id):
res.update({ line_vals_list[1]['sale_id'] = self.sale_id.id
'amount': so.amount_total, return line_vals_list
'currency_id': so.currency_id.id,
'payment_type': 'inbound',
'partner_id': so.partner_invoice_id.commercial_partner_id.id,
'partner_type': 'customer',
'communication': so.name,
'sale_id': so.id,
})
return res
def _compute_payment_amount(self, invoices=None, currency=None): def action_post(self):
amount = super(AccountAbstractPayment, self)._compute_payment_amount( super().action_post()
invoices=invoices, currency=currency) for pay in self:
if self.sale_id: if pay.sale_id and pay.payment_type == 'inbound':
payment_currency = currency pay._sale_down_payment_hook()
if not payment_currency:
payment_currency = self.sale_id.currency_id def _sale_down_payment_hook(self):
amount = float_round( # can be used for notifications
self.sale_id.amount_total - self.sale_id.amount_down_payment, # WAS on account.move.line on v12 ; is on account.payment on v14
precision_rounding=payment_currency.rounding) self.ensure_one()
return amount

View File

@@ -1,4 +1,4 @@
# Copyright 2019 Akretion France (http://www.akretion.com) # Copyright 2019-2024 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com> # @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
@@ -9,8 +9,8 @@ from odoo.tools import float_round
class SaleOrder(models.Model): class SaleOrder(models.Model):
_inherit = 'sale.order' _inherit = 'sale.order'
payment_line_ids = fields.One2many( payment_ids = fields.One2many(
'account.move.line', 'sale_id', string='Advance Payments', 'account.payment', 'sale_id', string='Advance Payments',
readonly=True) readonly=True)
amount_down_payment = fields.Monetary( amount_down_payment = fields.Monetary(
compute='_compute_amount_down_payment', string='Down Payment Amount') compute='_compute_amount_down_payment', string='Down Payment Amount')
@@ -19,30 +19,20 @@ class SaleOrder(models.Model):
compute='_compute_amount_down_payment', string='Residual') compute='_compute_amount_down_payment', string='Residual')
@api.depends( @api.depends(
'payment_line_ids.credit', 'payment_line_ids.debit', 'payment_ids.amount', 'payment_ids.currency_id', 'payment_ids.date',
'payment_line_ids.amount_currency', 'payment_line_ids.currency_id', 'payment_ids.state', 'currency_id')
'payment_line_ids.date', 'currency_id')
def _compute_amount_down_payment(self): def _compute_amount_down_payment(self):
for sale in self: for sale in self:
down_payment = 0.0 down_payment = 0.0
sale_currency = sale.pricelist_id.currency_id sale_currency = sale.currency_id
if sale_currency == sale.company_id.currency_id: prec_rounding = sale_currency.rounding or 0.01
for pl in sale.payment_line_ids: for payment in sale.payment_ids:
down_payment -= pl.balance if payment.payment_type == 'inbound' and payment.state == 'posted':
else: down_payment += payment.currency_id._convert(
for pl in sale.payment_line_ids: payment.amount, sale_currency, sale.company_id,
if ( payment.date)
pl.currency_id and
pl.currency_id == sale_currency and
pl.amount_currency):
down_payment -= pl.amount_currency
else:
down_payment -= sale.company_id.currency_id._convert(
pl.balance, sale_currency, sale.company_id,
pl.date)
down_payment = float_round( down_payment = float_round(
down_payment, precision_rounding=sale.currency_id.rounding) down_payment, precision_rounding=prec_rounding)
sale.amount_down_payment = down_payment sale.amount_down_payment = down_payment
sale.amount_residual = float_round( sale.amount_residual = float_round(
sale.amount_total - down_payment, sale.amount_total - down_payment, precision_rounding=prec_rounding)
precision_rounding=sale.currency_id.rounding)

View File

@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_payment_register_sale,Full access on account.payment.register.sale,model_account_payment_register_sale,account.group_account_invoice,1,1,1,1
access_account_payment_sale_user,Full access on account.payment to sale user,account.model_account_payment,sales_team.group_sale_salesman,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_payment_register_sale Full access on account.payment.register.sale model_account_payment_register_sale account.group_account_invoice 1 1 1 1
3 access_account_payment_sale_user Full access on account.payment to sale user account.model_account_payment sales_team.group_sale_salesman 1 1 1 1

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2019 Akretion France (http://www.akretion.com/) Copyright 2019-2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com> @author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
@@ -13,10 +13,12 @@
<field name="model">account.move.line</field> <field name="model">account.move.line</field>
<field name="inherit_id" ref="account.view_move_line_form"/> <field name="inherit_id" ref="account.view_move_line_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="invoice_id" position="after"> <xpath expr="//field[@name='blocked']/.." position="after">
<field name="sale_id" attrs="{'invisible': [('account_internal_type', '!=', 'receivable')]}" domain="['|', ('partner_id', 'child_of', partner_id), ('partner_invoice_id', 'child_of', partner_id), ('state', '!=', 'cancel'), ('invoice_status', '!=', 'invoiced')]"/> <group string="Sale Order" name="sale" attrs="{'invisible': [('account_internal_type', '!=', 'receivable')]}">
<field name="account_internal_type" invisible="1"/> <field name="sale_id"/>
</field> <field name="account_internal_type" invisible="1"/>
</group>
</xpath>
</field> </field>
</record> </record>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2018 Akretion France (http://www.akretion.com/) Copyright 2018-2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com> @author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
@@ -8,13 +8,13 @@
<odoo> <odoo>
<record id="view_account_payment_sale_form" model="ir.ui.view"> <record id="view_account_payment_form" model="ir.ui.view">
<field name="name">account.payment.sale.form</field> <field name="name">account.payment.sale.form</field>
<field name="model">account.payment</field> <field name="model">account.payment</field>
<field name="inherit_id" ref="account.view_account_payment_invoice_form"/> <field name="inherit_id" ref="account.view_account_payment_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="invoice_ids" position="after"> <field name="destination_account_id" position="after">
<field name="sale_id" invisible="1"/> <field name="sale_id" attrs="{'invisible': [('is_internal_transfer', '=', True)]}"/>
</field> </field>
</field> </field>
</record> </record>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
Copyright 2019 Akretion France (http://www.akretion.com/) Copyright 2019-2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com> @author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
--> -->
@@ -8,14 +8,6 @@
<odoo> <odoo>
<record id="sale_account_payment_action" model="ir.actions.act_window">
<field name="name">Register Payment</field>
<field name="res_model">account.payment</field>
<field name="view_mode">form</field>
<field name="view_id" ref="account.view_account_payment_invoice_form"/>
<field name="target">new</field>
</record>
<record id="view_order_form" model="ir.ui.view"> <record id="view_order_form" model="ir.ui.view">
<field name="name">advance_payment.sale.order.form</field> <field name="name">advance_payment.sale.order.form</field>
<field name="model">sale.order</field> <field name="model">sale.order</field>
@@ -23,7 +15,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<notebook position="inside"> <notebook position="inside">
<page name="advance_payment" string="Advance Payments"> <page name="advance_payment" string="Advance Payments">
<field name="payment_line_ids" nolabel="1"/> <field name="payment_ids" nolabel="1" colspan="2"/>
<field name="amount_residual" invisible="1"/> <field name="amount_residual" invisible="1"/>
</page> </page>
</notebook> </notebook>
@@ -34,7 +26,7 @@
<field name="amount_down_payment" nolabel="1" class="oe_subtotal_footer_separator"/> <field name="amount_down_payment" nolabel="1" class="oe_subtotal_footer_separator"/>
</field> </field>
<button name="action_cancel" position="before"> <button name="action_cancel" position="before">
<button type="action" name="%(sale_account_payment_action)d" string="Register Payment" attrs="{'invisible': ['|', ('amount_residual', '&lt;=', 0), ('invoice_status', '=', 'invoiced')]}"/> <button type="action" name="%(sale_down_payment.account_payment_register_sale_action)d" string="Register Payment" attrs="{'invisible': ['|', ('amount_residual', '&lt;=', 0), ('invoice_status', '=', 'invoiced')]}"/>
</button> </button>
</field> </field>
</record> </record>

View File

@@ -1 +1,2 @@
from . import account_bank_statement_sale from . import account_payment_register_sale
# from . import account_bank_statement_sale

View File

@@ -64,7 +64,9 @@ class AccountBankStatementSale(models.TransientModel):
self.ensure_one() self.ensure_one()
for line in self.line_ids: for line in self.line_ids:
if line.move_line_id.sale_id != line.sale_id: if line.move_line_id.sale_id != line.sale_id:
line.move_line_id.sale_id = line.sale_id.id line.move_line_id.write({'sale_id': line.sale_id.id or False})
if line.sale_id:
line.move_line_id._sale_down_payment_hook()
class AccountBankStatementSaleLine(models.TransientModel): class AccountBankStatementSaleLine(models.TransientModel):

View File

@@ -0,0 +1,62 @@
# Copyright 2024 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountPaymentRegisterSale(models.TransientModel):
_name = 'account.payment.register.sale'
_description = "Register a payment from a sale.order"
_check_company_auto = True
sale_id = fields.Many2one(
"sale.order", string="Sale Order",
check_company=True, readonly=True, required=True)
company_id = fields.Many2one('res.company', required=True)
journal_id = fields.Many2one(
'account.journal', string="Journal", check_company=True, required=True,
domain="[('company_id', '=', company_id), ('type', 'in', ('bank', 'cash'))]")
amount = fields.Monetary(required=True)
currency_id = fields.Many2one('res.currency', required=True)
date = fields.Date(default=fields.Date.context_today, required=True)
ref = fields.Char()
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if self._context.get('active_model') == 'sale.order' and self._context.get('active_id'):
sale = self.env['sale.order'].browse(self._context['active_id'])
res.update({
'sale_id': sale.id,
'company_id': sale.company_id.id,
'amount': sale.amount_total,
'currency_id': sale.currency_id.id,
})
return res
def run(self):
self.ensure_one()
pay_vals = {
'company_id': self.company_id.id,
'sale_id': self.sale_id.id,
'date': self.date,
'amount': self.amount,
'payment_type': 'inbound',
'partner_type': 'customer',
'ref': self.ref,
'journal_id': self.journal_id.id,
'currency_id': self.currency_id.id,
'partner_id': self.sale_id.partner_invoice_id.commercial_partner_id.id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
}
payment = self.env['account.payment'].create(pay_vals)
payment.action_post()
@api.onchange("journal_id")
def journal_id_change(self):
if (
self.journal_id and
self.journal_id.currency_id and
self.journal_id.currency_id != self.currency_id):
self.currency_id = self.journal_id.currency_id.id

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="account_payment_register_sale_form" model="ir.ui.view">
<field name="model">account.payment.register.sale</field>
<field name="arch" type="xml">
<form>
<group name="main">
<field name="sale_id" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="date"/>
<field name="journal_id" widget="selection"/>
<label for="amount"/>
<div name="amount" class="o_row">
<field name="amount"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
</div>
<field name="ref"/>
</group>
<footer>
<button name="run" type="object" string="Register Payment" class="btn-primary"/>
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="account_payment_register_sale_action" model="ir.actions.act_window">
<field name="name">Register Payment from Sale Order</field>
<field name="res_model">account.payment.register.sale</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@@ -1,2 +1 @@
from . import sale from . import models
from . import sale_report

View File

@@ -4,12 +4,12 @@
{ {
'name': 'Sale Margin No Onchange', "name": "Sale Margin No Onchange",
'version': '12.0.1.0.0', "version": "14.0.1.0.0",
'category': 'Sales', "category": "Sales",
'license': 'AGPL-3', "license": "AGPL-3",
'summary': 'Copy standard price on sale order line and compute margins', "summary": "Copy standard price on sale order line and compute margins",
'description': """ "description": """
This module copies the field *standard_price* of the product on the sale order line when the sale order line is created and then computes the margin of the sale order and the sale order line (in the currency of the quotation, in the currency of the company and the margin rate). This module copies the field *standard_price* of the product on the sale order line when the sale order line is created and then computes the margin of the sale order and the sale order line (in the currency of the quotation, in the currency of the company and the margin rate).
I decided to develop this module as an alternative to the OCA sale margin modules because I wanted a small and simple module. The module *account_invoice_margin*, available in the same Github repository, do the same thing on customer invoices. I decided to develop this module as an alternative to the OCA sale margin modules because I wanted a small and simple module. The module *account_invoice_margin*, available in the same Github repository, do the same thing on customer invoices.
@@ -17,9 +17,9 @@ I decided to develop this module as an alternative to the OCA sale margin module
This module has been written by Alexis de Lattre from Akretion This module has been written by Alexis de Lattre from Akretion
<alexis.delattre@akretion.com>. <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', "author": "Akretion",
'website': 'http://www.akretion.com', "website": "http://www.akretion.com",
'depends': ['sale'], "depends": ["sale"],
'data': ['sale_view.xml'], "data": ["views/sale_view.xml"],
'installable': False, "installable": True,
} }

View File

@@ -0,0 +1,2 @@
from . import sale
from . import sale_report

View File

@@ -4,7 +4,6 @@
from odoo import api, fields, models from odoo import api, fields, models
import odoo.addons.decimal_precision as dp
class SaleOrderLine(models.Model): class SaleOrderLine(models.Model):
@@ -16,23 +15,23 @@ class SaleOrderLine(models.Model):
store=True, string='Company Currency') store=True, string='Company Currency')
standard_price_company_currency = fields.Float( standard_price_company_currency = fields.Float(
string='Cost Price in Company Currency', readonly=True, string='Cost Price in Company Currency', readonly=True,
digits=dp.get_precision('Product Price'), digits="Product Price",
help="Cost price in company currency in the unit of measure " help="Cost price in company currency in the unit of measure "
"of the sale order line") "of the sale order line")
standard_price_sale_currency = fields.Float( standard_price_sale_currency = fields.Float(
string='Cost Price in Sale Currency', readonly=True, string='Cost Price in Sale Currency',
compute='_compute_margin', store=True, compute='_compute_margin', store=True,
digits=dp.get_precision('Product Price'), digits="Product Price",
help="Cost price in sale currency in the unit of measure " help="Cost price in sale currency in the unit of measure "
"of the sale order line") "of the sale order line")
margin_sale_currency = fields.Monetary( margin_sale_currency = fields.Monetary(
string='Margin in Sale Currency', readonly=True, store=True, string='Margin in Sale Currency', store=True,
compute='_compute_margin', currency_field='currency_id') compute='_compute_margin', currency_field='currency_id')
margin_company_currency = fields.Monetary( margin_company_currency = fields.Monetary(
string='Margin in Company Currency', readonly=True, store=True, string='Margin in Company Currency', store=True,
compute='_compute_margin', currency_field='company_currency_id') compute='_compute_margin', currency_field='company_currency_id')
margin_rate = fields.Float( margin_rate = fields.Float(
string="Margin Rate", readonly=True, store=True, string="Margin Rate", store=True,
compute='_compute_margin', compute='_compute_margin',
digits=(16, 2), help="Margin rate in percentage of the sale price") digits=(16, 2), help="Margin rate in percentage of the sale price")
@@ -68,19 +67,20 @@ class SaleOrderLine(models.Model):
line.margin_rate = margin_rate line.margin_rate = margin_rate
# We want to copy standard_price on sale order line # We want to copy standard_price on sale order line
@api.model @api.model_create_multi
def create(self, vals): def create(self, vals_list):
if vals.get('product_id'): for vals in vals_list:
pp = self.env['product.product'].browse(vals['product_id']) if vals.get('product_id'):
std_price = pp.standard_price pp = self.env['product.product'].browse(vals['product_id'])
sale_uom_id = vals.get('product_uom') std_price = pp.standard_price
if sale_uom_id and sale_uom_id != pp.uom_id.id: sale_uom_id = vals.get('product_uom')
sale_uom = self.env['uom.uom'].browse(sale_uom_id) if sale_uom_id and sale_uom_id != pp.uom_id.id:
# convert from product UoM to sale UoM sale_uom = self.env['uom.uom'].browse(sale_uom_id)
std_price = pp.uom_id._compute_price( # convert from product UoM to sale UoM
pp.standard_price, sale_uom) std_price = pp.uom_id._compute_price(
vals['standard_price_company_currency'] = std_price pp.standard_price, sale_uom)
return super(SaleOrderLine, self).create(vals) vals['standard_price_company_currency'] = std_price
return super().create(vals_list)
def write(self, vals): def write(self, vals):
if not vals: if not vals:
@@ -101,7 +101,7 @@ class SaleOrderLine(models.Model):
if sale_uom != pp.uom_id: if sale_uom != pp.uom_id:
std_price = pp.uom_id._compute_price(std_price, sale_uom) std_price = pp.uom_id._compute_price(std_price, sale_uom)
sol.write({'standard_price_company_currency': std_price}) sol.write({'standard_price_company_currency': std_price})
return super(SaleOrderLine, self).write(vals) return super().write(vals)
class SaleOrder(models.Model): class SaleOrder(models.Model):
@@ -114,21 +114,27 @@ class SaleOrder(models.Model):
margin_sale_currency = fields.Monetary( margin_sale_currency = fields.Monetary(
string='Margin in Sale Currency', string='Margin in Sale Currency',
currency_field='currency_id', currency_field='currency_id',
readonly=True, compute='_compute_margin', store=True) compute='_compute_margin', store=True)
margin_company_currency = fields.Monetary( margin_company_currency = fields.Monetary(
string='Margin in Company Currency', string='Margin in Company Currency',
currency_field='company_currency_id', currency_field='company_currency_id',
readonly=True, compute='_compute_margin', store=True) compute='_compute_margin', store=True)
@api.depends( @api.depends(
'order_line.margin_sale_currency', 'order_line.margin_sale_currency',
'order_line.margin_company_currency') 'order_line.margin_company_currency')
def _compute_margin(self): def _compute_margin(self):
rg_res = self.env['sale.order.line'].read_group(
[('order_id', 'in', self.ids)],
['order_id', 'margin_sale_currency:sum', 'margin_company_currency:sum'],
['order_id'])
mapped_data = dict([
(x['order_id'][0], {
'margin_sale_currency': x['margin_sale_currency'],
'margin_company_currency': x['margin_company_currency'],
}) for x in rg_res])
for order in self: for order in self:
margin_sale_cur = 0.0 order.margin_sale_currency = mapped_data.get(
margin_comp_cur = 0.0 order.id, {}).get('margin_sale_currency')
for sol in order.order_line: order.margin_company_currency = mapped_data.get(
margin_sale_cur += sol.margin_sale_currency order.id, {}).get('margin_company_currency')
margin_comp_cur += sol.margin_company_currency
order.margin_sale_currency = margin_sale_cur
order.margin_company_currency = margin_comp_cur

View File

@@ -20,7 +20,8 @@
groups="account.group_account_user"/> groups="account.group_account_user"/>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
</group> </group>
<xpath expr="//field[@name='order_line']/form//field[@name='analytic_tag_ids']/.." position="after"> <xpath expr="//field[@name='order_line']/form/group/group//field[@name='analytic_tag_ids']/.." position="after">
<group>
<field name="standard_price_sale_currency" groups="base.group_no_one"/> <field name="standard_price_sale_currency" groups="base.group_no_one"/>
<field name="standard_price_company_currency" groups="base.group_no_one"/> <field name="standard_price_company_currency" groups="base.group_no_one"/>
<field name="margin_sale_currency" groups="base.group_no_one"/> <field name="margin_sale_currency" groups="base.group_no_one"/>
@@ -31,6 +32,7 @@
</div> </div>
<field name="company_currency_id" invisible="1"/> <field name="company_currency_id" invisible="1"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
</group>
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@@ -1 +1,2 @@
from . import sale_order from . import sale_order
from . import sale_report

View File

@@ -0,0 +1,20 @@
# Copyright 2024 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SaleReport(models.Model):
_inherit = "sale.report"
route_id = fields.Many2one('stock.location.route', string='Route', readonly=True)
def _select_additional_fields(self, fields):
fields['route_id'] = ", s.route_id AS route_id"
return super()._select_additional_fields(fields)
def _group_by_sale(self, groupby=''):
res = super()._group_by_sale(groupby=groupby)
res += ', s.route_id'
return res

View File

@@ -49,6 +49,14 @@ entry and link it to product product -->
<field name="context">{}</field> <field name="context">{}</field>
</record> </record>
<!-- ACCOUNT -->
<record id="account.product_product_menu_sellable" model="ir.ui.menu">
<field name="action" ref="product_product_action_sell"/>
</record>
<record id="account.product_product_menu_purchasable" model="ir.ui.menu">
<field name="action" ref="product_product_action_purchased"/>
</record>
<!-- Create a product template menu entry in configuration --> <!-- Create a product template menu entry in configuration -->
<menuitem id="sale_config_product_template_menu" action="product.product_template_action" <menuitem id="sale_config_product_template_menu" action="product.product_template_action"

View File

@@ -23,6 +23,7 @@
'views/account_move.xml', 'views/account_move.xml',
'views/res_company.xml', 'views/res_company.xml',
"views/res_partner.xml", "views/res_partner.xml",
"views/sale_template.xml",
'wizards/sale_invoice_discount_all_lines_view.xml', 'wizards/sale_invoice_discount_all_lines_view.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],

View File

@@ -135,3 +135,29 @@ class SaleOrderLine(models.Model):
if no_product_code_param and no_product_code_param == 'True': if no_product_code_param and no_product_code_param == 'True':
product = product.with_context(display_default_code=False) product = product.with_context(display_default_code=False)
return super().get_sale_order_line_multiline_description_sale(product) return super().get_sale_order_line_multiline_description_sale(product)
# In v12, I developped the 3 modules service_line_qty_update_base, service_line_qty_update_purchase
# and service_line_qty_update_sale that add a wizard to update service lines and track the changes
# in the chatter.
# In v14, you can edit the quantity of the service lines directly and the purchase module
# tracks changes in the chatter... but the sale module doesn't track the changes of 'qty_delivered'
# So I "ported" that native feature of the purchase module to sale.order.line... here it is !
# We can remove that code if this feature is added in the sale module (it's NOT the case in
# odoo v17)
def write(self, vals):
if 'qty_delivered' in vals:
for line in self:
line._track_qty_delivered(vals['qty_delivered'])
return super().write(vals)
def _track_qty_delivered(self, new_qty):
self.ensure_one()
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
if (
float_compare(new_qty, self.qty_delivered, precision_digits=prec) and
self.order_id.state == 'sale'):
self.order_id.message_post_with_view(
'sale_usability.track_so_line_qty_delivered_template',
values={'line': self, 'qty_delivered': new_qty},
subtype_id=self.env.ref('mail.mt_note').id
)

View File

@@ -59,6 +59,10 @@
</field> </field>
<field name="state" position="attributes"> <field name="state" position="attributes">
<attribute name="invisible">0</attribute> <attribute name="invisible">0</attribute>
<attribute name="optional">show</attribute>
<attribute name="widget">badge</attribute>
<attribute name="decoration-success">state == 'done'</attribute>
<attribute name="decoration-info">state == 'sale'</attribute>
</field> </field>
<field name="partner_id" position="after"> <field name="partner_id" position="after">
<field name="client_order_ref" optional="show"/> <field name="client_order_ref" optional="show"/>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="track_so_line_qty_delivered_template">
<div>
<strong>The delivered quantity has been updated.</strong>
<ul>
<li><t t-esc="line.name"/>:</li>
Delivered Quantity: <t t-esc="line.qty_delivered" /> -&gt; <t t-esc="float(qty_delivered)"/><br/>
</ul>
</div>
</template>
</odoo>

View File

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

View File

@@ -1,18 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Service Line Qty Update Base',
'version': '12.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Update delivery qty on service lines - Base module',
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': ['product'],
'data': [
'wizard/service_qty_update_view.xml',
],
'installable': False,
}

View File

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

View File

@@ -1,70 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.tools import float_compare, float_is_zero
import odoo.addons.decimal_precision as dp
from odoo.exceptions import UserError
class ServiceQtyUpdate(models.TransientModel):
_name = 'service.qty.update'
_description = 'Wizard to update delivery qty on service lines'
line_ids = fields.One2many('service.qty.update.line', 'parent_id', string="Lines")
def run(self):
self.ensure_one()
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for line in self.line_ids:
if float_compare(line.post_delivered_qty, line.order_qty, precision_digits=prec) > 0:
raise UserError(_(
"On line '%s', the total delivered qty (%s) is superior to the ordered qty (%s).") % (line.name, line.post_delivered_qty, line.order_qty))
fc_added = float_compare(line.added_delivered_qty, 0, precision_digits=prec)
if fc_added < 0:
raise UserError(_(
"On line '%s', the added quantity is negative.") % line.name)
if fc_added > 0:
line.process_line()
return True
class ServiceQtyUpdateLine(models.TransientModel):
_name = 'service.qty.update.line'
_description = 'Lines of the wizard that updates delivery qty on service lines'
parent_id = fields.Many2one(
'service.qty.update', string='Wizard', ondelete='cascade')
product_id = fields.Many2one('product.product', string='Product', readonly=True)
name = fields.Char()
name_readonly = fields.Char(related='name', string='Description')
order_qty = fields.Float(
string='Order Qty',
digits=dp.get_precision('Product Unit of Measure'))
order_qty_readonly = fields.Float(related='order_qty', string='Product Unit of Measure')
pre_delivered_qty = fields.Float(
digits=dp.get_precision('Product Unit of Measure'))
pre_delivered_qty_readonly = fields.Float(related='pre_delivered_qty', string='Current Delivered Qty')
added_delivered_qty = fields.Float(
string='Added Delivered Qty',
digits=dp.get_precision('Product Unit of Measure'))
post_delivered_qty = fields.Float(
compute='_compute_post_delivered_qty',
string='Total Delivered Qty',
digits=dp.get_precision('Product Unit of Measure'))
uom_id = fields.Many2one('uom.uom', string='UoM', readonly=True)
comment = fields.Char(string='Comment')
@api.depends('pre_delivered_qty', 'added_delivered_qty')
def _compute_post_delivered_qty(self):
for line in self:
line.post_delivered_qty = line.pre_delivered_qty + line.added_delivered_qty
def process_line(self):
# Write and message_post
return
# sale : qty_delivered
# purchase : qty_received

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="service_qty_update_form" model="ir.ui.view">
<field name="model">service.qty.update</field>
<field name="arch" type="xml">
<form>
<group name="main">
<field name="line_ids" nolabel="1">
<tree editable="bottom">
<field name="product_id"/>
<field name="name" invisible="0"/>
<field name="name_readonly"/>
<field name="order_qty" invisible="1"/>
<field name="order_qty_readonly"/>
<field name="pre_delivered_qty" invisible="1"/>
<field name="pre_delivered_qty_readonly"/>
<field name="added_delivered_qty"/>
<field name="post_delivered_qty"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="comment"/>
</tree>
</field>
</group>
<footer>
<button name="run" type="object" string="Validate" class="btn-primary"/>
<button special="cancel" string="Cancel" class="btn-default"/>
</footer>
</form>
</field>
</record>
<record id="service_qty_update_action" model="ir.actions.act_window">
<field name="name">Service Order Lines - Update Delivered Qty</field>
<field name="res_model">service.qty.update</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

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

View File

@@ -1,22 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Service Line Qty Update Purchase',
'version': '12.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Update delivery qty on service lines - Purchase module',
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': [
'purchase',
'service_line_qty_update_base',
'purchase_reception_status',
],
'data': [
'views/purchase_order.xml',
],
'installable': False,
}

View File

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

View File

@@ -1,21 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
has_service = fields.Boolean(compute='_compute_has_service')
@api.depends('order_line.product_id.type')
def _compute_has_service(self):
for order in self:
has_service = False
for l in order.order_line:
if l.product_id.type == 'service':
has_service = True
break
order.has_service = has_service

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="purchase_order_form" model="ir.ui.view">
<field name="name">purchase.order.form</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<button name="action_view_invoice" position="after">
<button name="%(service_line_qty_update_base.service_qty_update_action)d" type="action" string="Update Service Qty" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('has_service', '=', False), ('reception_status', '=', 'received')]}" groups="purchase.group_purchase_user"/>
<field name="has_service" invisible="1"/>
</button>
<xpath expr="//field[@name='order_line']/tree/field[@name='qty_received']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

@@ -1,62 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.tools import float_compare
from odoo.exceptions import UserError
class ServiceQtyUpdate(models.TransientModel):
_inherit = 'service.qty.update'
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
if self._context.get('active_model') == 'purchase.order' and self._context.get('active_id'):
lines = []
order = self.env['purchase.order'].browse(self._context['active_id'])
for l in order.order_line.filtered(lambda x: x.product_id.type == 'service'):
if float_compare(l.product_qty, l.qty_received, precision_digits=prec) > 0:
lines.append((0, 0, {
'purchase_line_id': l.id,
'product_id': l.product_id.id,
'name': l.name,
'name_readonly': l.name,
'order_qty': l.product_qty,
'order_qty_readonly': l.product_qty,
'pre_delivered_qty': l.qty_received,
'pre_delivered_qty_readonly': l.qty_received,
'uom_id': l.product_uom.id,
}))
if lines:
res['line_ids'] = lines
else:
raise UserError(_(
"All service lines are fully received."))
return res
class ServiceQtyUpdateLine(models.TransientModel):
_inherit = 'service.qty.update.line'
purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Line', readonly=True)
def process_line(self):
po_line = self.purchase_line_id
if po_line:
new_qty = po_line.qty_received + self.added_delivered_qty
po_line.write({'qty_received': new_qty})
body = """
<p>Received qty updated on service line <b>%s</b>:
<ul>
<li>Added received qty: <b>%s</b></li>
<li>Total received qty: %s</li>
</ul></p>
""" % (self.name, self.added_delivered_qty, new_qty)
if self.comment:
body += '<p>Comment: %s</p>' % self.comment
po_line.order_id.message_post(body=body)
return super().process_line()

View File

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

View File

@@ -1,22 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Service Line Qty Update Sale',
'version': '12.0.1.0.0',
'category': 'Tools',
'license': 'AGPL-3',
'summary': 'Update delivery qty on service lines - Sale module',
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': [
'sale',
'service_line_qty_update_base',
# 'purchase_reception_status',
],
'data': [
'views/sale_order.xml',
],
'installable': False,
}

View File

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

View File

@@ -1,21 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
has_service = fields.Boolean(compute='_compute_has_service')
@api.depends('order_line.product_id.type')
def _compute_has_service(self):
for order in self:
has_service = False
for l in order.order_line:
if l.product_id.type == 'service':
has_service = True
break
order.has_service = has_service

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 Akretion France (http://www.akretion.com/)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_order_form" model="ir.ui.view">
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<button name="action_quotation_send" position="after">
<button name="%(service_line_qty_update_base.service_qty_update_action)d" type="action" string="Update Service Qty" attrs="{'invisible': ['|', ('state', 'not in', ('sale', 'done')), ('has_service', '=', False)]}" groups="sales_team.group_sale_salesman"/>
<field name="has_service" invisible="1"/>
</button>
<xpath expr="//field[@name='order_line']/tree/field[@name='qty_delivered']" position="attributes">
<attribute name="readonly">1</attribute>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

@@ -1,62 +0,0 @@
# Copyright 2020 Akretion France (http://www.akretion.com/)
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.tools import float_compare
from odoo.exceptions import UserError
class ServiceQtyUpdate(models.TransientModel):
_inherit = 'service.qty.update'
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
if self._context.get('active_model') == 'sale.order' and self._context.get('active_id'):
lines = []
order = self.env['sale.order'].browse(self._context['active_id'])
for l in order.order_line.filtered(lambda x: x.product_id.type == 'service'):
if float_compare(l.product_qty, l.qty_delivered, precision_digits=prec) > 0:
lines.append((0, 0, {
'sale_line_id': l.id,
'product_id': l.product_id.id,
'name': l.name,
'name_readonly': l.name,
'order_qty': l.product_uom_qty,
'order_qty_readonly': l.product_uom_qty,
'pre_delivered_qty': l.qty_delivered,
'pre_delivered_qty_readonly': l.qty_delivered,
'uom_id': l.product_uom.id,
}))
if lines:
res['line_ids'] = lines
else:
raise UserError(_(
"All service lines are fully delivered."))
return res
class ServiceQtyUpdateLine(models.TransientModel):
_inherit = 'service.qty.update.line'
sale_line_id = fields.Many2one('sale.order.line', string='Sale Line', readonly=True)
def process_line(self):
so_line = self.sale_line_id
if so_line:
new_qty = so_line.qty_delivered + self.added_delivered_qty
so_line.write({'qty_delivered': new_qty})
body = """
<p>Delivered qty updated on service line <b>%s</b>:
<ul>
<li>Added delivered qty: <b>%s</b></li>
<li>Total delivered qty: %s</li>
</ul></p>
""" % (self.name, self.added_delivered_qty, new_qty)
if self.comment:
body += '<p>Comment: %s</p>' % self.comment
so_line.order_id.message_post(body=body)
return super().process_line()

View File

@@ -0,0 +1,2 @@
from . import models
from .hooks import post_init_hook

View File

@@ -0,0 +1,14 @@
# Copyright 2024 Akretion
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Stock Location Simple",
"summary": "Simplified stock.location menu",
"version": "14.0.1.0.0",
"license": "AGPL-3",
"author": "Akretion",
"website": "http://akretion.com",
"depends": ["stock"],
"data": ["views/stock_location_views.xml"],
"post_init_hook": "post_init_hook",
}

View File

@@ -0,0 +1,10 @@
# Copyright 2021 Akretion (https://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import SUPERUSER_ID, api
def post_init_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
env["stock.warehouse"].search([])._check_locations_created_by_warehouse()

View File

@@ -0,0 +1,2 @@
from . import stock_location
from . import stock_warehouse

View File

@@ -0,0 +1,10 @@
# Copyright 2024 Akretion
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
class StockLocation(models.Model):
_inherit = "stock.location"
is_created_by_warehouse = fields.Boolean()

View File

@@ -0,0 +1,34 @@
# Copyright 2024 Akretion
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
import pprint
class StockWarehouse(models.Model):
_inherit = "stock.warehouse"
def _get_location_fields(self):
location_fields = []
for field, definition in self.fields_get().items():
if definition.get("relation") == "stock.location":
location_fields.append(field)
return location_fields
def _check_locations_created_by_warehouse(self):
location_ids = self.env["stock.location"]
location_fields = self._get_location_fields()
for rec in self:
for field in location_fields:
location_ids |= getattr(rec, field)
location_ids.write({"is_created_by_warehouse": True})
@api.model
def create(self, vals):
res = super().create(vals)
res._check_locations_created_by_warehouse()
return res

View File

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

View File

@@ -0,0 +1,19 @@
# Copyright 2018-2022 Camptocamp
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests import TransactionCase
class TestStockLocationSimple(TransactionCase):
def setUp(self):
super().setUp()
self.env["stock.warehouse"].search([])._check_locations_created_by_warehouse()
def test_location_checked_at_warehouse_creation(self):
warehouse = self.env["stock.warehouse"].create({"name": "Test", "code": "TEST"})
self.assertTrue(warehouse.view_location_id.is_created_by_warehouse)
def test_native_location_checked(self):
location_id = self.env.ref("stock.warehouse0").view_location_id
self.assertTrue(location_id.is_created_by_warehouse)

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2024 Akretion
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="stock_location_simple_form_view" model="ir.ui.view">
<field name="name">stock.location.simple.form</field>
<field name="model">stock.location</field>
<field name="arch" type="xml">
<form string="Stock Location">
<sheet>
<div class="oe_button_box" name="button_box">
<button string="Current Stock"
class="oe_stat_button"
icon="fa-cubes" name="%(stock.location_open_quants)d" type="action"/>
</div>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
<label for="location_id" class="oe_edit_only"/>
<h2>
<field name="location_id" required="1" domain="[('usage', '=', 'internal')]"/>
</h2>
<group>
<field name="active" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<field name="comment" placeholder="External note..."/>
</sheet>
</form>
</field>
</record>
<record id="stock_location_simple_tree_view" model="ir.ui.view">
<field name="name">stock.location.simple.tree</field>
<field name="model">stock.location</field>
<field name="arch" type="xml">
<!-- TODO: decoration info if lockdown, if view, if linked to a warehouse -->
<tree string="Stock Location">
<field name="active" invisible="1"/>
<field name="complete_name" string="Location"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="stock_location_simple_act_window" model="ir.actions.act_window">
<field name="name">Stock Location</field>
<field name="res_model">stock.location</field>
<field name="view_mode">tree,form</field>
<field name="domain">[("usage", "=", "internal")]</field>
</record>
<record id="stock_location_simple_act_window_tree" model="ir.actions.act_window.view" >
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="stock_location_simple_tree_view"/>
<field name="act_window_id" ref="stock_location_simple_act_window"/>
</record>
<record id="stock_location_simple_act_window_form" model="ir.actions.act_window.view" >
<field name="sequence" eval="3"/>
<field name="view_mode">form</field>
<field name="view_id" ref="stock_location_simple_form_view"/>
<field name="act_window_id" ref="stock_location_simple_act_window"/>
</record>
<record id="stock_location_simple_menu" model="ir.ui.menu">
<field name="name">Locations</field>
<field name="parent_id" ref="stock.menu_warehouse_config"/>
<field name="action" ref="stock_location_simple_act_window"/>
<field name="sequence" eval="0"/>
<field name="groups_id" eval="[(4, ref('stock.group_stock_multi_locations'))]" />
</record>
<!-- Modify name and groups_id on original stock.location menu -->
<record id="stock.menu_action_location_form" model="ir.ui.menu">
<field name="name">Locations Technical</field>
<field name="parent_id" ref="stock.menu_warehouse_config"/>
<field name="action" ref="stock.action_location_form"/>
<field name="sequence" eval="2"/>
<field name="groups_id" eval="[(4, ref('base.group_erp_manager'))]" />
</record>
</odoo>

View File

@@ -4,7 +4,7 @@
{ {
'name': 'Stock No Product Template Menu', 'name': 'Stock No Product Template Menu',
'version': '12.0.1.0.0', 'version': '14.0.1.0.0',
'category': 'Stock', 'category': 'Stock',
'license': 'AGPL-3', 'license': 'AGPL-3',
'summary': "Replace product.template menu entries by product.product menu entries", 'summary': "Replace product.template menu entries by product.product menu entries",
@@ -19,7 +19,7 @@ This module also switches to the tree view by default for Product menu entries,
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>. This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""", """,
'author': 'Akretion', 'author': 'Akretion',
'website': 'http://www.akretion.com', 'website': 'https://github.com/akretion/odoo-usability',
'depends': ['stock'], 'depends': ['stock'],
'data': ['view.xml'], 'data': ['view.xml'],
'installable': True, 'installable': True,

View File

@@ -11,6 +11,9 @@
<record id="stock.menu_product_variant_config_stock" model="ir.ui.menu"> <record id="stock.menu_product_variant_config_stock" model="ir.ui.menu">
<field name="action" ref="product.product_normal_action"/> <field name="action" ref="product.product_normal_action"/>
</record> </record>
<!-- we don't care about:
"search_default_consumable": 1 => not useful
"default_type": 'product' : stock_usability make it the default... no need to bother
-->
</odoo> </odoo>

View File

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

View File

@@ -0,0 +1,31 @@
# Copyright 2024 Akretion (http://www.akretion.com)
# @author Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Stock Picking Batch Usability',
'version': '14.0.1.0.0',
'category': 'Inventory, Logistic, Storage',
'license': 'AGPL-3',
'summary': 'Several usability enhancements in Batch Pickings',
'description': """
Stock Picking Batch Usability
=============================
The usability enhancements include:
* add batch_id on picking form view
* when creating a batch from a list of pickings, raise an error if a picking is already linked to a batch.
* when creating a batch from a list of pickings, display the form view of the batch after validation of the wizard
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
""",
'author': 'Akretion',
'website': 'https://github.com/akretion/odoo-usability',
'depends': ['stock_picking_batch'],
'data': [
'views/stock_picking.xml',
],
'installable': True,
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2014-2020 Akretion (http://www.akretion.com/)
@author Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_picking_form" model="ir.ui.view">
<field name="name">stock_picking_batch_usability.stock.picking_form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form" />
<field name="arch" type="xml">
<group name="other_infos" position="inside">
<field name="batch_id" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"/>
</group>
</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,42 @@
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, fields, _
from odoo.exceptions import UserError
class StockPickingToBatch(models.TransientModel):
_inherit = 'stock.picking.to.batch'
# add 'in_progress' in domain
batch_id = fields.Many2one(domain="[('state', 'in', ('draft', 'in_progress'))]")
mode = fields.Selection(default='new')
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
pickings = self.env['stock.picking'].browse(self.env.context.get('active_ids'))
for picking in pickings:
if picking.batch_id:
raise UserError(_(
"The picking %(picking)s is already part of batch %(batch)s.",
picking=picking.display_name,
batch=picking.batch_id.display_name))
return res
def attach_pickings(self):
super().attach_pickings()
if self.mode == 'new':
pickings = self.env['stock.picking'].browse(self.env.context.get('active_ids'))
batch_id = pickings[0].batch_id.id
elif self.mode == 'existing':
batch_id = self.batch_id.id
else:
raise UserError('It should never happen')
action = self.env["ir.actions.actions"]._for_xml_id("stock_picking_batch.stock_picking_batch_action")
action.update({
'view_mode': 'form,tree',
'res_id': batch_id,
'views': False,
})
return action

View File

@@ -0,0 +1,271 @@
diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py
index b0b3c8f2662..7fefa873014 100644
--- a/addons/mrp/models/product.py
+++ b/addons/mrp/models/product.py
@@ -195,6 +195,7 @@ class ProductProduct(models.Model):
ratios_qty_available = []
ratios_incoming_qty = []
ratios_outgoing_qty = []
+ ratios_reserved_qty = []
ratios_free_qty = []
for component, bom_sub_lines in bom_sub_lines_grouped.items():
@@ -219,6 +220,7 @@ class ProductProduct(models.Model):
"qty_available": float_round(component.qty_available, precision_rounding=rounding),
"incoming_qty": float_round(component.incoming_qty, precision_rounding=rounding),
"outgoing_qty": float_round(component.outgoing_qty, precision_rounding=rounding),
+ "reserved_qty": float_round(component.reserved_qty, precision_rounding=rounding),
"free_qty": float_round(component.free_qty, precision_rounding=rounding),
}
)
@@ -226,6 +228,7 @@ class ProductProduct(models.Model):
ratios_qty_available.append(component_res["qty_available"] / qty_per_kit)
ratios_incoming_qty.append(component_res["incoming_qty"] / qty_per_kit)
ratios_outgoing_qty.append(component_res["outgoing_qty"] / qty_per_kit)
+ ratios_reserved_qty.append(component_res["reserved_qty"] / qty_per_kit)
ratios_free_qty.append(component_res["free_qty"] / qty_per_kit)
if bom_sub_lines and ratios_virtual_available: # Guard against all cnsumable bom: at least one ratio should be present.
res[product.id] = {
@@ -233,6 +236,7 @@ class ProductProduct(models.Model):
'qty_available': min(ratios_qty_available) * bom_kits[product].product_qty // 1,
'incoming_qty': min(ratios_incoming_qty) * bom_kits[product].product_qty // 1,
'outgoing_qty': min(ratios_outgoing_qty) * bom_kits[product].product_qty // 1,
+ 'reserved_qty': min(ratios_reserved_qty) * bom_kits[product].product_qty // 1,
'free_qty': min(ratios_free_qty) * bom_kits[product].product_qty // 1,
}
else:
@@ -241,6 +245,7 @@ class ProductProduct(models.Model):
'qty_available': 0,
'incoming_qty': 0,
'outgoing_qty': 0,
+ 'reserved_qty': 0,
'free_qty': 0,
}
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index e0b9b2f95d6..f50fb39eb5a 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -50,6 +50,11 @@ class Product(models.Model):
"of its children.\n"
"Otherwise, this includes goods stored in any Stock Location "
"with 'internal' type.")
+ reserved_qty = fields.Float(
+ compute='_compute_quantities', compute_sudo=False,
+ digits='Product Unit of Measure',
+ search='_search_reserved_qty',
+ string='Reserved Quantity')
free_qty = fields.Float(
'Free To Use Quantity ', compute='_compute_quantities', search='_search_free_qty',
digits='Product Unit of Measure', compute_sudo=False,
@@ -107,6 +112,7 @@ class Product(models.Model):
product.incoming_qty = res[product.id]['incoming_qty']
product.outgoing_qty = res[product.id]['outgoing_qty']
product.virtual_available = res[product.id]['virtual_available']
+ product.reserved_qty = res[product.id]['reserved_qty']
product.free_qty = res[product.id]['free_qty']
# Services need to be set with 0.0 for all quantities
services = self - products
@@ -114,6 +120,7 @@ class Product(models.Model):
services.incoming_qty = 0.0
services.outgoing_qty = 0.0
services.virtual_available = 0.0
+ services.reserved_qty = 0.0
services.free_qty = 0.0
def _product_available(self, field_names=None, arg=False):
@@ -170,7 +177,7 @@ class Product(models.Model):
product_id = product.id
if not product_id:
res[product_id] = dict.fromkeys(
- ['qty_available', 'free_qty', 'incoming_qty', 'outgoing_qty', 'virtual_available'],
+ ['qty_available', 'reserved_qty', 'free_qty', 'incoming_qty', 'outgoing_qty', 'virtual_available'],
0.0,
)
continue
@@ -182,6 +189,7 @@ class Product(models.Model):
qty_available = quants_res.get(product_id, [0.0])[0]
reserved_quantity = quants_res.get(product_id, [False, 0.0])[1]
res[product_id]['qty_available'] = float_round(qty_available, precision_rounding=rounding)
+ res[product_id]['reserved_qty'] = float_round(reserved_quantity, precision_rounding=rounding)
res[product_id]['free_qty'] = float_round(qty_available - reserved_quantity, precision_rounding=rounding)
res[product_id]['incoming_qty'] = float_round(moves_in_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['outgoing_qty'] = float_round(moves_out_res.get(product_id, 0.0), precision_rounding=rounding)
@@ -308,13 +316,16 @@ class Product(models.Model):
# TDE FIXME: should probably clean the search methods
return self._search_product_quantity(operator, value, 'outgoing_qty')
+ def _search_reserved_qty(self, operator, value):
+ return self._search_product_quantity(operator, value, 'reserved_qty')
+
def _search_free_qty(self, operator, value):
return self._search_product_quantity(operator, value, 'free_qty')
def _search_product_quantity(self, operator, value, field):
# TDE FIXME: should probably clean the search methods
# to prevent sql injections
- if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty', 'free_qty'):
+ if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty', 'reserved_qty', 'free_qty'):
raise UserError(_('Invalid domain left operand %s', field))
if operator not in ('<', '>', '=', '!=', '<=', '>='):
raise UserError(_('Invalid domain operator %s', operator))
@@ -464,6 +475,13 @@ class Product(models.Model):
action['domain'] = [('product_id', '=', self.id)]
return action
+ def action_view_reserved_stock_move_lines(self):
+ action = self.action_view_stock_move_lines()
+ action['domain'].append(('state', 'not in', ('done', 'draft', 'cancel')))
+ # stock.stock_move_line_action sets search_default_done to 1, so we force it to 0
+ action['context'] = dict(self._context, search_default_done=0)
+ return action
+
def action_view_related_putaway_rules(self):
self.ensure_one()
domain = [
@@ -616,6 +634,18 @@ class ProductTemplate(models.Model):
qty_available = fields.Float(
'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
compute_sudo=False, digits='Product Unit of Measure')
+ reserved_qty = fields.Float(
+ compute='_compute_quantities', compute_sudo=False,
+ search='_search_reserved_qty',
+ digits='Product Unit of Measure',
+ string='Reserved Quantity')
+ free_qty = fields.Float(
+ compute='_compute_quantities', compute_sudo=False,
+ search='_search_free_qty',
+ digits='Product Unit of Measure',
+ string='Free To Use Quantity',
+ help="The free to use quantity corresponds to the quantity on hand "
+ "- reserved quantity")
virtual_available = fields.Float(
'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available',
compute_sudo=False, digits='Product Unit of Measure')
@@ -667,6 +697,8 @@ class ProductTemplate(models.Model):
res = self._compute_quantities_dict()
for template in self:
template.qty_available = res[template.id]['qty_available']
+ template.reserved_qty = res[template.id]['reserved_qty']
+ template.free_qty = res[template.id]['free_qty']
template.virtual_available = res[template.id]['virtual_available']
template.incoming_qty = res[template.id]['incoming_qty']
template.outgoing_qty = res[template.id]['outgoing_qty']
@@ -680,16 +712,22 @@ class ProductTemplate(models.Model):
prod_available = {}
for template in self:
qty_available = 0
+ reserved_qty = 0
+ free_qty = 0
virtual_available = 0
incoming_qty = 0
outgoing_qty = 0
for p in template.product_variant_ids:
qty_available += variants_available[p.id]["qty_available"]
+ reserved_qty += variants_available[p.id]["reserved_qty"]
+ free_qty += variants_available[p.id]["free_qty"]
virtual_available += variants_available[p.id]["virtual_available"]
incoming_qty += variants_available[p.id]["incoming_qty"]
outgoing_qty += variants_available[p.id]["outgoing_qty"]
prod_available[template.id] = {
"qty_available": qty_available,
+ "reserved_qty": reserved_qty,
+ "free_qty": free_qty,
"virtual_available": virtual_available,
"incoming_qty": incoming_qty,
"outgoing_qty": outgoing_qty,
@@ -717,6 +755,16 @@ class ProductTemplate(models.Model):
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
+ def _search_reserved_qty(self, operator, value):
+ domain = [('reserved_qty', operator, value)]
+ product_variants = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variants.ids)]
+
+ def _search_free_qty(self, operator, value):
+ domain = [('free_qty', operator, value)]
+ product_variants = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variants.ids)]
+
def _search_virtual_available(self, operator, value):
domain = [('virtual_available', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
@@ -859,6 +907,13 @@ class ProductTemplate(models.Model):
action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
return action
+ def action_view_reserved_stock_move_lines(self):
+ action = self.action_view_stock_move_lines()
+ action['domain'].append(('state', 'not in', ('done', 'draft', 'cancel')))
+ # stock.stock_move_line_action sets search_default_done to 1, so we force it to 0
+ action['context'] = dict(self._context, search_default_done=0)
+ return action
+
def action_open_product_lot(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_production_lot_form")
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index 7655c15ac09..0897952c79d 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -40,7 +40,9 @@
<field name="arch" type="xml">
<field name="price" position="after">
<field name="qty_available" attrs="{'invisible':[('type', '!=', 'product')]}" optional="show" decoration-danger="virtual_available &lt; 0" decoration-warning="virtual_available == 0" decoration-bf="1"/>
- <field name="virtual_available" attrs="{'invisible':[('type', '!=', 'product')]}" string="Forecasted Quantity" optional="show" decoration-danger="virtual_available &lt; 0" decoration-warning="virtual_available == 0"/>
+ <field name="reserved_qty" attrs="{'invisible': [('type', '!=', 'product')]}" optional="show"/>
+ <field name="free_qty" attrs="{'invisible': [('type', '!=', 'product')]}" optional="show" string="Free Qty"/>
+ <field name="virtual_available" attrs="{'invisible':[('type', '!=', 'product')]}" string="Forecasted Quantity" optional="hide" decoration-danger="virtual_available &lt; 0" decoration-warning="virtual_available == 0"/>
</field>
</field>
</record>
@@ -53,7 +55,9 @@
<field name="uom_id" position="before">
<field name="show_on_hand_qty_status_button" invisible="1"/>
<field name="qty_available" attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}" optional="show" decoration-danger="qty_available &lt; 0"/>
- <field name="virtual_available" attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}" optional="show" decoration-danger="virtual_available &lt; 0" decoration-bf="1"/>
+ <field name="reserved_qty" attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}" optional="show"/>
+ <field name="free_qty" attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}" optional="show" string="Free Qty"/>
+ <field name="virtual_available" attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}" optional="hide" decoration-danger="virtual_available &lt; 0" decoration-bf="1"/>
</field>
<field name="default_code" position="after">
<field name="responsible_id" widget="many2one_avatar_user"/>
@@ -234,6 +238,18 @@
</span>
<span class="o_stat_text">On Hand</span>
</div>
+ </button>
+ <button class="oe_stat_button"
+ name="action_view_reserved_stock_move_lines"
+ icon="fa-building-o"
+ type="object" attrs="{'invisible': [('type', '!=', 'product')]}">
+ <div class="o_field_widget o_stat_info">
+ <span class="o_stat_value">
+ <field name="reserved_qty" widget="statinfo" nolabel="1" class="mr4"/>
+ <field name="uom_name"/>
+ </span>
+ <span class="o_stat_text">Reserved</span>
+ </div>
</button>
<button type="object"
name="action_product_forecast_report"
@@ -324,6 +340,18 @@
<span class="o_stat_text">On Hand</span>
</div>
</button>
+ <button type="object"
+ name="action_view_reserved_stock_move_lines"
+ attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}"
+ class="oe_stat_button" icon="fa-cubes">
+ <div class="o_field_widget o_stat_info">
+ <span class="o_stat_value" widget="statinfo">
+ <field name="reserved_qty" widget="statinfo" nolabel="1" class="mr4"/>
+ <field name="uom_name"/>
+ </span>
+ <span class="o_stat_text">Reserved</span>
+ </div>
+ </button>
<button type="object"
name="action_product_tmpl_forecast_report"
attrs="{'invisible':[('show_on_hand_qty_status_button', '=', False)]}"

View File

@@ -1,238 +0,0 @@
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index bbb6f301834..48d016010dc 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -36,6 +36,18 @@ class Product(models.Model):
"or any of its children.\n"
"Otherwise, this includes goods stored in any Stock Location "
"with 'internal' type.")
+ reserved_qty = fields.Float(
+ compute='_compute_quantities',
+ digits=dp.get_precision('Product Unit of Measure'),
+ search='_search_reserved_qty',
+ string='Reserved Quantity')
+ free_qty = fields.Float(
+ compute='_compute_quantities',
+ search='_search_free_qty',
+ digits=dp.get_precision('Product Unit of Measure'),
+ string='Free To Use Quantity',
+ help="The free to use quantity corresponds to the quantity on hand "
+ "- reserved quantity")
virtual_available = fields.Float(
'Forecast Quantity', compute='_compute_quantities', search='_search_virtual_available',
digits=dp.get_precision('Product Unit of Measure'),
@@ -84,6 +96,8 @@ class Product(models.Model):
product.incoming_qty = res[product.id]['incoming_qty']
product.outgoing_qty = res[product.id]['outgoing_qty']
product.virtual_available = res[product.id]['virtual_available']
+ product.reserved_qty = res[product.id]['reserved_qty']
+ product.free_qty = res[product.id]['free_qty']
def _product_available(self, field_names=None, arg=False):
""" Compatibility method """
@@ -124,7 +138,7 @@ class Product(models.Model):
domain_move_out_todo = [('state', 'in', ('waiting', 'confirmed', 'assigned', 'partially_available'))] + domain_move_out
moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
- quants_res = dict((item['product_id'][0], item['quantity']) for item in Quant.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id'))
+ quants_res = dict((item['product_id'][0], (item['quantity'], item['reserved_quantity'])) for item in Quant.read_group(domain_quant, ['product_id', 'quantity', 'reserved_quantity'], ['product_id'], orderby='id'))
if dates_in_the_past:
# Calculate the moves that were done before now to calculate back in time (as most questions will be recent ones)
domain_move_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done
@@ -138,10 +152,13 @@ class Product(models.Model):
rounding = product.uom_id.rounding
res[product_id] = {}
if dates_in_the_past:
- qty_available = quants_res.get(product_id, 0.0) - moves_in_res_past.get(product_id, 0.0) + moves_out_res_past.get(product_id, 0.0)
+ qty_available = quants_res.get(product_id, [0.0])[0] - moves_in_res_past.get(product_id, 0.0) + moves_out_res_past.get(product_id, 0.0)
else:
- qty_available = quants_res.get(product_id, 0.0)
+ qty_available = quants_res.get(product_id, [0.0])[0]
+ reserved_quantity = quants_res.get(product_id, [False, 0.0])[1]
res[product_id]['qty_available'] = float_round(qty_available, precision_rounding=rounding)
+ res[product_id]['reserved_qty'] = float_round(reserved_quantity, precision_rounding=rounding)
+ res[product_id]['free_qty'] = float_round(qty_available - reserved_quantity, precision_rounding=rounding)
res[product_id]['incoming_qty'] = float_round(moves_in_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['outgoing_qty'] = float_round(moves_out_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['virtual_available'] = float_round(
@@ -261,10 +278,16 @@ class Product(models.Model):
# TDE FIXME: should probably clean the search methods
return self._search_product_quantity(operator, value, 'outgoing_qty')
+ def _search_reserved_qty(self, operator, value):
+ return self._search_product_quantity(operator, value, 'reserved_qty')
+
+ def _search_free_qty(self, operator, value):
+ return self._search_product_quantity(operator, value, 'free_qty')
+
def _search_product_quantity(self, operator, value, field):
# TDE FIXME: should probably clean the search methods
# to prevent sql injections
- if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'):
+ if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty', 'reserved_qty', 'free_qty'):
raise UserError(_('Invalid domain left operand %s') % field)
if operator not in ('<', '>', '=', '!=', '<=', '>='):
raise UserError(_('Invalid domain operator %s') % operator)
@@ -387,6 +410,12 @@ class Product(models.Model):
action['context'] = {'search_default_internal_loc': 1}
return action
+ def action_open_move_lines(self):
+ action = self.env.ref('stock.stock_move_line_action').read()[0]
+ action['domain'] = [('product_id', '=', self.id), ('state', 'not in', ('done', 'cancel'))]
+ action['context'] = {'default_product_id': self.id}
+ return action
+
def action_open_product_lot(self):
self.ensure_one()
action = self.env.ref('stock.action_production_lot_form').read()[0]
@@ -451,6 +480,18 @@ class ProductTemplate(models.Model):
qty_available = fields.Float(
'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
digits=dp.get_precision('Product Unit of Measure'))
+ reserved_qty = fields.Float(
+ compute='_compute_quantities',
+ digits=dp.get_precision('Product Unit of Measure'),
+ search='_search_reserved_qty',
+ string='Reserved Quantity')
+ free_qty = fields.Float(
+ compute='_compute_quantities',
+ search='_search_free_qty',
+ digits=dp.get_precision('Product Unit of Measure'),
+ string='Free To Use Quantity',
+ help="The free to use quantity corresponds to the quantity on hand "
+ "- reserved quantity")
virtual_available = fields.Float(
'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available',
digits=dp.get_precision('Product Unit of Measure'))
@@ -490,6 +531,8 @@ class ProductTemplate(models.Model):
res = self._compute_quantities_dict()
for template in self:
template.qty_available = res[template.id]['qty_available']
+ template.reserved_qty = res[template.id]['reserved_qty']
+ template.free_qty = res[template.id]['free_qty']
template.virtual_available = res[template.id]['virtual_available']
template.incoming_qty = res[template.id]['incoming_qty']
template.outgoing_qty = res[template.id]['outgoing_qty']
@@ -503,16 +546,22 @@ class ProductTemplate(models.Model):
prod_available = {}
for template in self:
qty_available = 0
+ reserved_qty = 0
+ free_qty = 0
virtual_available = 0
incoming_qty = 0
outgoing_qty = 0
for p in template.product_variant_ids:
qty_available += variants_available[p.id]["qty_available"]
+ reserved_qty += variants_available[p.id]["reserved_qty"]
+ free_qty += variants_available[p.id]["free_qty"]
virtual_available += variants_available[p.id]["virtual_available"]
incoming_qty += variants_available[p.id]["incoming_qty"]
outgoing_qty += variants_available[p.id]["outgoing_qty"]
prod_available[template.id] = {
"qty_available": qty_available,
+ "reserved_qty": reserved_qty,
+ "free_qty": free_qty,
"virtual_available": virtual_available,
"incoming_qty": incoming_qty,
"outgoing_qty": outgoing_qty,
@@ -524,6 +573,16 @@ class ProductTemplate(models.Model):
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
+ def _search_reserved_qty(self, operator, value):
+ domain = [('reserved_qty', operator, value)]
+ product_variant_ids = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variant_ids.ids)]
+
+ def _search_free_qty(self, operator, value):
+ domain = [('free_qty', operator, value)]
+ product_variant_ids = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variant_ids.ids)]
+
def _search_virtual_available(self, operator, value):
domain = [('virtual_available', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
@@ -609,6 +668,13 @@ class ProductTemplate(models.Model):
action['context'] = {'search_default_internal_loc': 1}
return action
+ def action_open_move_lines(self):
+ products = self.mapped('product_variant_ids')
+ action = self.env.ref('stock.stock_move_line_action').read()[0]
+ action['domain'] = [('product_id', 'in', products.ids), ('state', 'not in', ('done', 'cancel'))]
+ action['context'] = {}
+ return action
+
def action_view_orderpoints(self):
products = self.mapped('product_variant_ids')
action = self.env.ref('stock.product_open_orderpoint').read()[0]
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index 70321544e00..99bd47b4e56 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -36,6 +36,8 @@
</tree>
<field name="price" position="after">
<field name="qty_available" attrs="{'invisible':[('type', '!=', 'product')]}"/>
+ <field name="reserved_qty" attrs="{'invisible': [('type', '!=', 'product')]}"/>
+ <field name="free_qty" attrs="{'invisible': [('type', '!=', 'product')]}"/>
<field name="virtual_available" attrs="{'invisible':[('type', '!=', 'product')]}"/>
</field>
</field>
@@ -52,6 +54,8 @@
</tree>
<field name="uom_id" position="before">
<field name="qty_available" attrs="{'invisible':[('type', '!=', 'product')]}"/>
+ <field name="reserved_qty" attrs="{'invisible': [('type', '!=', 'product')]}"/>
+ <field name="free_qty" attrs="{'invisible': [('type', '!=', 'product')]}"/>
<field name="virtual_available" attrs="{'invisible':[('type', '!=', 'product')]}"/>
</field>
</field>
@@ -140,6 +144,7 @@
</field>
<ul position="inside">
<li t-if="record.type.raw_value == 'product'">On hand: <field name="qty_available"/> <field name="uom_id"/></li>
+ <li t-if="record.type.raw_value == 'product'">Reserved: <field name="reserved_qty"/> <field name="uom_id"/></li>
</ul>
</field>
</record>
@@ -208,6 +213,18 @@
<span class="o_stat_text">On Hand</span>
</div>
</button>
+ <button class="oe_stat_button"
+ name="action_open_move_lines"
+ icon="fa-building-o"
+ type="object" attrs="{'invisible': [('type', '!=', 'product')]}">
+ <div class="o_field_widget o_stat_info">
+ <span class="o_stat_value">
+ <field name="reserved_qty" widget="statinfo" nolabel="1" class="mr4"/>
+ <field name="uom_name"/>
+ </span>
+ <span class="o_stat_text">Reserved</span>
+ </div>
+ </button>
<button type="action"
name="%(stock.action_stock_level_forecast_report_product)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
@@ -291,6 +308,18 @@
<span class="o_stat_text">On Hand</span>
</div>
</button>
+ <button type="object"
+ name="action_open_move_lines"
+ attrs="{'invisible': [('type', '!=', 'product')]}"
+ class="oe_stat_button" icon="fa-building-o">
+ <div class="o_field_widget o_stat_info">
+ <span class="o_stat_value" widget="statinfo">
+ <field name="reserved_qty" widget="statinfo" nolabel="1" class="mr4"/>
+ <field name="uom_name"/>
+ </span>
+ <span class="o_stat_text">Reserved</span>
+ </div>
+ </button>
<button type="action"
name="%(stock.action_stock_level_forecast_report_template)d"
attrs="{'invisible':[('type', '!=', 'product')]}"

View File

@@ -80,7 +80,6 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="reference" position="before"> <field name="reference" position="before">
<field name="picking_id" attrs="{'invisible': [('picking_id', '=', False)]}"/> <field name="picking_id" attrs="{'invisible': [('picking_id', '=', False)]}"/>
<field name="production_id" attrs="{'invisible': [('production_id', '=', False)]}"/>
</field> </field>
</field> </field>
</record> </record>

View File

@@ -56,6 +56,10 @@
<xpath expr="//sheet/group/group/field[@name='location_dest_id']" position="attributes"> <xpath expr="//sheet/group/group/field[@name='location_dest_id']" position="attributes">
<attribute name="attrs">{}</attribute> <attribute name="attrs">{}</attribute>
</xpath> </xpath>
<group name="other_infos" position="inside">
<!-- The field picking_type_id is displayed under partner_id but invisible when hide_picking_type = True. But I want it to be always visible... but not at such a visible place ! So I display it in other infos when hide_picking_type = False -->
<field name="picking_type_id" attrs="{'invisible': [('hide_picking_type', '=', False)], 'readonly': [('state', '!=', 'draft')]}"/>
</group>
</field> </field>
</record> </record>