Compare commits
3 Commits
14.0-add-s
...
14.0-accou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b2f746aa | ||
|
|
e38606992a | ||
|
|
c89f1b74dc |
@@ -1 +1,2 @@
|
||||
from . import models
|
||||
from . import account_invoice
|
||||
from . import account_invoice_report
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{
|
||||
'name': 'Account Invoice Margin',
|
||||
'version': '14.0.1.0.0',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Invoicing Management',
|
||||
'license': 'AGPL-3',
|
||||
'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>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['account'],
|
||||
'data': [
|
||||
'views/account_move.xml',
|
||||
'account_invoice_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
152
account_invoice_margin/account_invoice.py
Normal file
152
account_invoice_margin/account_invoice.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# 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']
|
||||
60
account_invoice_margin/account_invoice_report.py
Normal file
60
account_invoice_margin/account_invoice_report.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# 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
|
||||
51
account_invoice_margin/account_invoice_view.xml
Normal file
51
account_invoice_margin/account_invoice_view.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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>
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import account_move
|
||||
from . import account_invoice_report
|
||||
@@ -1,36 +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)
|
||||
|
||||
# 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
|
||||
@@ -1,155 +0,0 @@
|
||||
# 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')
|
||||
@@ -1,55 +0,0 @@
|
||||
<?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>
|
||||
@@ -11,8 +11,7 @@
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="button_draft" position="before">
|
||||
<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 name="prepare_update_wizard" type="object" string="Update Invoice" states="posted" groups="account.group_account_invoice"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -21,7 +21,6 @@ class AccountMoveUpdate(models.TransientModel):
|
||||
invoice_payment_term_id = fields.Many2one(
|
||||
'account.payment.term', string='Payment Term')
|
||||
ref = fields.Char(string='Reference') # field label is customized in the view
|
||||
invoice_date = fields.Date()
|
||||
invoice_origin = fields.Char(string='Source Document')
|
||||
partner_bank_id = fields.Many2one(
|
||||
'res.partner.bank', string='Bank Account')
|
||||
@@ -31,7 +30,7 @@ class AccountMoveUpdate(models.TransientModel):
|
||||
@api.model
|
||||
def _simple_fields2update(self):
|
||||
'''List boolean, date, datetime, char, text fields'''
|
||||
return ['ref', 'invoice_origin', 'invoice_date']
|
||||
return ['ref', 'invoice_origin']
|
||||
|
||||
@api.model
|
||||
def _m2o_fields2update(self):
|
||||
|
||||
@@ -15,15 +15,13 @@
|
||||
<field name="move_type" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="partner_id" invisible="1"/>
|
||||
<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="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="Ref" attrs="{'invisible': [('move_type', '!=', 'entry')]}" name="ref"/>
|
||||
<field name="invoice_origin" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
|
||||
<field name="invoice_origin"/>
|
||||
<!-- update of payment term is broken -->
|
||||
<!-- <field name="invoice_payment_term_id" widget="selection"/>-->
|
||||
<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}" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
|
||||
<field name="partner_bank_id"/>
|
||||
<field name="user_id" options="{'no_open': True, 'no_create': True, 'no_create_edit': True}"/>
|
||||
</group>
|
||||
<group name="lines">
|
||||
<field name="line_ids" nolabel="1" widget="section_and_note_one2many">
|
||||
@@ -32,8 +30,8 @@
|
||||
<field name="display_type" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
|
||||
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
|
||||
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)]}"/>
|
||||
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)]}"/>
|
||||
<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"/>
|
||||
</tree>
|
||||
|
||||
4
account_journal_display_type/__init__.py
Normal file
4
account_journal_display_type/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import models
|
||||
from .hook import post_init_hook
|
||||
17
account_journal_display_type/__manifest__.py
Normal file
17
account_journal_display_type/__manifest__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright 2024 Akretion (https://www.akretion.com).
|
||||
# @author Matthieu SAISON <matthieu.saison@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
{
|
||||
"name": "Account Journal Display Type",
|
||||
"summary": "Account Journal type for payment",
|
||||
"version": "14.0.1.0.0",
|
||||
"development_status": "Beta",
|
||||
"category": "Accounting",
|
||||
"website": "https://github.com/OCA/account-financial-tools",
|
||||
"author": "Akretion, " "Odoo Community Association (OCA)",
|
||||
"maintainers": ["matthieu_saison"],
|
||||
"license": "AGPL-3",
|
||||
"depends": ["account", "account_statement_completion_label_simple"],
|
||||
"data": ["views/account_journal_view.xml"],
|
||||
"post_init_hook": "post_init_hook",
|
||||
}
|
||||
8
account_journal_display_type/hook.py
Normal file
8
account_journal_display_type/hook.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
|
||||
def post_init_hook(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
journal_ids = env["account.journal"].search([])
|
||||
for journal in journal_ids:
|
||||
journal.display_type = journal.type
|
||||
1
account_journal_display_type/models/__init__.py
Normal file
1
account_journal_display_type/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import account_journal
|
||||
80
account_journal_display_type/models/account_journal.py
Normal file
80
account_journal_display_type/models/account_journal.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
display_type = fields.Selection(
|
||||
[
|
||||
("sale", "Sales"),
|
||||
("purchase", "Purchase"),
|
||||
("cash", "Cash"),
|
||||
("bank", "Bank"),
|
||||
("payment", "Payment"),
|
||||
("general", "Miscellaneous"),
|
||||
],
|
||||
required=True,
|
||||
help="Select 'Sale' for customer invoices journals.\n"
|
||||
"Select 'Purchase' for vendor bills journals.\n"
|
||||
"Select 'Cash' or 'Bank' for journals that are used in customer or vendor payments.\n"
|
||||
"Select 'General' for miscellaneous operations journals.",
|
||||
)
|
||||
type = fields.Selection(compute="_compute_type", store=True)
|
||||
default_account_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
compute="_compute_default_account_id",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
payment_debit_account_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
compute="_compute_payment_account_id",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
payment_credit_account_id = fields.Many2one(
|
||||
comodel_name="account.account",
|
||||
compute="_compute_payment_account_id",
|
||||
readonly=False,
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends("display_type")
|
||||
def _compute_type(self):
|
||||
for record in self:
|
||||
if record.display_type == "payment":
|
||||
record.type = "bank"
|
||||
else:
|
||||
record.type = record.display_type
|
||||
|
||||
@api.depends("display_type", "payment_debit_account_id")
|
||||
def _compute_default_account_id(self):
|
||||
for record in self:
|
||||
if record.display_type == "payment":
|
||||
record.default_account_id = record.payment_debit_account_id
|
||||
|
||||
@api.depends("display_type", "default_account_id")
|
||||
def _compute_payment_account_id(self):
|
||||
for record in self:
|
||||
if record.type == "cash":
|
||||
record.payment_debit_account_id = record.default_account_id
|
||||
record.payment_credit_account_id = record.default_account_id
|
||||
|
||||
@api.model
|
||||
def _fill_missing_values(self, vals):
|
||||
# _fill_missing_values automaticly create a account if not set,
|
||||
# this code bypass this behavior
|
||||
if vals.get("display_type") == "payment":
|
||||
vals["default_account_id"] = True
|
||||
elif vals.get("display_type") == "cash":
|
||||
vals["payment_debit_account_id"] = True
|
||||
vals["payment_credit_account_id"] = True
|
||||
super()._fill_missing_values(vals)
|
||||
if vals.get("display_type") == "payment":
|
||||
vals.pop("default_account_id")
|
||||
# allow journal creation if display_type not define
|
||||
if not vals.get("display_type"):
|
||||
vals["display_type"] = vals["type"]
|
||||
elif vals.get("display_type") == "cash":
|
||||
vals.pop("payment_debit_account_id")
|
||||
vals.pop("payment_credit_account_id")
|
||||
@@ -0,0 +1 @@
|
||||
* Matthieu Saison <matthieu.saison@akretion.com>
|
||||
@@ -0,0 +1,8 @@
|
||||
This module introduce improvement on journal type to simplify configuration of Payment and Cash Journal.
|
||||
|
||||
|
||||
The payment journal does not exist in odoo, it's associate to a bank journal but in payment case,
|
||||
the default_account_id will have the same value as payment_debit_account_id.
|
||||
This module introduce a new field type called display_type who hide the default type from UI, and make possible to had new journal type. Payment display_type is hadded here, and the associated legacy journal type is bank. The default_account_id is hided and will have the same value as payment_debit_account_id
|
||||
|
||||
For Cash type, the only field we kept is default_account_id. The payment config tab is useless for a cash journal and was remove from UI. (in this case payment_debit_account_id and payment_credit_account_id are set with value of default_account_id)
|
||||
1
account_journal_display_type/tests/__init__.py
Normal file
1
account_journal_display_type/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_journal_display_type
|
||||
@@ -0,0 +1,81 @@
|
||||
from odoo.tests import SavepointCase
|
||||
from odoo.tests.common import new_test_user, users
|
||||
|
||||
|
||||
class TestJournalDisplayType(SavepointCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.account = self.env["account.account"].create(
|
||||
{
|
||||
"name": "Test account",
|
||||
"code": "TAC",
|
||||
"user_type_id": self.env.ref("account.data_account_type_payable").id,
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
self.manager = new_test_user(
|
||||
self.env, "test_manager", "account.group_account_manager"
|
||||
)
|
||||
|
||||
@users("test_manager")
|
||||
def test_journal_payment_with_account(self):
|
||||
|
||||
journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "Bank with account",
|
||||
"display_type": "cash",
|
||||
"type": "cash",
|
||||
"code": "BK100",
|
||||
"payment_debit_account_id": self.account.id,
|
||||
"payment_credit_account_id": self.account.id,
|
||||
}
|
||||
)
|
||||
journal.display_type = "payment"
|
||||
self.assertEqual(journal.type, "bank")
|
||||
self.assertEqual(journal.default_account_id, journal.payment_debit_account_id)
|
||||
|
||||
@users("test_manager")
|
||||
def test_journal_payment_without_account(self):
|
||||
|
||||
journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "Bank without account",
|
||||
"display_type": "payment",
|
||||
"type": "bank",
|
||||
"code": "BK101",
|
||||
}
|
||||
)
|
||||
self.assertTrue(journal.payment_debit_account_id)
|
||||
self.assertTrue(journal.payment_credit_account_id)
|
||||
self.assertEqual(journal.type, "bank")
|
||||
self.assertEqual(journal.default_account_id, journal.payment_debit_account_id)
|
||||
|
||||
@users("test_manager")
|
||||
def test_journal_cash_with_account(self):
|
||||
|
||||
journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "Cash with account",
|
||||
"display_type": "cash",
|
||||
"type": "cash",
|
||||
"code": "BK102",
|
||||
"default_account_id": self.account.id,
|
||||
}
|
||||
)
|
||||
self.assertEqual(journal.type, "cash")
|
||||
self.assertEqual(journal.default_account_id, self.account)
|
||||
self.assertEqual(journal.default_account_id, journal.payment_debit_account_id)
|
||||
self.assertEqual(journal.default_account_id, journal.payment_credit_account_id)
|
||||
|
||||
@users("test_manager")
|
||||
def test_journal_bank_without_account(self):
|
||||
|
||||
journal = self.env["account.journal"].create(
|
||||
{
|
||||
"name": "Bank without account",
|
||||
"type": "bank",
|
||||
"code": "BK103",
|
||||
}
|
||||
)
|
||||
self.assertTrue(journal.payment_debit_account_id)
|
||||
74
account_journal_display_type/views/account_journal_view.xml
Normal file
74
account_journal_display_type/views/account_journal_view.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_journal_form" model="ir.ui.view">
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form" />
|
||||
<field name="priority">1</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="type" position="attributes">
|
||||
<attribute name="attrs">{'invisible': True}</attribute>
|
||||
</field>
|
||||
<field name="type" position="after">
|
||||
<field name="display_type" string="Type"/>
|
||||
</field>
|
||||
|
||||
<xpath
|
||||
expr="//page[@name='bank_account']/group/group[1]/field[@name='default_account_id'][1]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{'required': [ ('type', '=', 'bank'), ('display_type', '=', 'bank')], 'invisible': [('display_type', '!=', 'bank')]}</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath
|
||||
expr="//page[@name='bank_account']/group/group[1]/field[@name='default_account_id'][2]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{'required': [('type', '=', 'cash')], 'invisible': [('type', '!=', 'cash')]}
|
||||
</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath
|
||||
expr="//page[@name='bank_account']/group/group[1]/field[@name='suspense_account_id'][1]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{'required': [('display_type', 'in', ('bank', 'cash'))], 'invisible': [('display_type', 'not in', ('bank', 'cash'))]}</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath
|
||||
expr="//page[@name='bank_account']/group/group[2]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{'invisible': ['|', ('type', '!=', 'bank'), ('display_type', '=', 'payment') ]}</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath
|
||||
expr="//page[@name='journal_entries']/group/group[1]/field[@name='payment_debit_account_id'][1]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{'required': [('type', 'in', ('bank', 'cash'))], 'invisible': [('type', 'not in', ('bank', 'cash'))]}</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath
|
||||
expr="//page[@name='journal_entries']/group/group[2]/field[@name='payment_credit_account_id'][1]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="attrs">
|
||||
{'required': [('type', 'in', ('bank', 'cash'))], 'invisible': [('type', 'not in', ('bank', 'cash'))]}</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//page[@name='journal_entries']" position="attributes">
|
||||
<attribute name="attrs">
|
||||
{'invisible': [('type', 'in', ['sale', 'purchase', 'general','cash'])]}</attribute>
|
||||
</xpath>
|
||||
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -30,7 +30,6 @@
|
||||
'views/product.xml',
|
||||
'views/res_config_settings.xml',
|
||||
'views/res_company.xml',
|
||||
'views/res_partner.xml',
|
||||
'views/account_report.xml',
|
||||
'views/account_reconcile_model.xml',
|
||||
'wizard/account_invoice_mark_sent_view.xml',
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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>
|
||||
@@ -6,8 +6,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-03-26 21:27+0000\n"
|
||||
"PO-Revision-Date: 2024-03-26 21:27+0000\n"
|
||||
"POT-Creation-Date: 2021-07-01 10:02+0000\n"
|
||||
"PO-Revision-Date: 2021-07-01 10:02+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -15,18 +15,6 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: \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
|
||||
#: model:ir.model,name:base_usability.model_res_partner_bank
|
||||
msgid "Bank Accounts"
|
||||
@@ -37,22 +25,11 @@ msgstr ""
|
||||
msgid "Bank Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: code:addons/base_usability/models/res_company.py:0
|
||||
#, python-format
|
||||
msgid "Capital:"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model,name:base_usability.model_res_company
|
||||
msgid "Companies"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.ir_property_view_search
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model,name:base_usability.model_res_partner
|
||||
msgid "Contact"
|
||||
@@ -71,7 +48,6 @@ msgid "Customer Number:"
|
||||
msgstr ""
|
||||
|
||||
#. 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_model__display_name
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_company__display_name
|
||||
@@ -89,24 +65,12 @@ msgstr ""
|
||||
msgid "E-mail:"
|
||||
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
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
|
||||
msgid "Group By"
|
||||
msgstr ""
|
||||
|
||||
#. 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_model__id
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_company__id
|
||||
@@ -123,7 +87,6 @@ msgid "Installable"
|
||||
msgstr ""
|
||||
|
||||
#. 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_model____last_update
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_company____last_update
|
||||
@@ -176,11 +139,6 @@ msgstr ""
|
||||
msgid "Partner Tags"
|
||||
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
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_users__ref
|
||||
@@ -188,14 +146,8 @@ msgid "Reference"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model,name:base_usability.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: code:addons/base_usability/models/res_company.py:0
|
||||
#, python-format
|
||||
msgid "SIRET:"
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
|
||||
msgid "Search Countries"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
@@ -217,14 +169,6 @@ msgstr ""
|
||||
msgid "Tel:"
|
||||
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
|
||||
#: model:ir.model,name:base_usability.model_res_users
|
||||
msgid "Users"
|
||||
|
||||
@@ -6,27 +6,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-03-26 21:27+0000\n"
|
||||
"PO-Revision-Date: 2024-03-26 21:27+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"POT-Creation-Date: 2021-07-01 10:02+0000\n"
|
||||
"PO-Revision-Date: 2021-07-01 12:15+0200\n"
|
||||
"Last-Translator: Alexis de Lattre <alexis@via.ecp.fr>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \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
|
||||
#: model:ir.model,name:base_usability.model_res_partner_bank
|
||||
msgid "Bank Accounts"
|
||||
@@ -37,22 +25,11 @@ msgstr "Comptes bancaires"
|
||||
msgid "Bank Name"
|
||||
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
|
||||
#: model:ir.model,name:base_usability.model_res_company
|
||||
msgid "Companies"
|
||||
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
|
||||
#: model:ir.model,name:base_usability.model_res_partner
|
||||
msgid "Contact"
|
||||
@@ -65,13 +42,11 @@ msgstr "Devise"
|
||||
|
||||
#. module: base_usability
|
||||
#: code:addons/base_usability/models/res_partner.py:0
|
||||
#: code:addons/base_usability/models/res_partner.py:0
|
||||
#, python-format
|
||||
msgid "Customer Number:"
|
||||
msgstr "N° client :"
|
||||
|
||||
#. 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_model__display_name
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_company__display_name
|
||||
@@ -89,24 +64,12 @@ msgstr "Nom affiché"
|
||||
msgid "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
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
|
||||
msgid "Group By"
|
||||
msgstr "Grouper par"
|
||||
|
||||
#. 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_model__id
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_company__id
|
||||
@@ -115,7 +78,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_users__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
msgstr "ID"
|
||||
|
||||
#. module: base_usability
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.view_module_filter
|
||||
@@ -123,7 +86,6 @@ msgid "Installable"
|
||||
msgstr "Installable"
|
||||
|
||||
#. 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_model____last_update
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_company____last_update
|
||||
@@ -174,12 +136,7 @@ msgstr "Personne (utilisé pour cacher des entrées de menu natifs)"
|
||||
#. module: base_usability
|
||||
#: model:ir.model,name:base_usability.model_res_partner_category
|
||||
msgid "Partner Tags"
|
||||
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é"
|
||||
msgstr "Étiquettes du partenaire"
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
|
||||
@@ -188,18 +145,11 @@ msgid "Reference"
|
||||
msgstr "Référence"
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model,name:base_usability.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
|
||||
msgid "Search Countries"
|
||||
msgstr ""
|
||||
|
||||
#. 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
|
||||
#, python-format
|
||||
msgid "Supplier Number:"
|
||||
@@ -208,7 +158,7 @@ msgstr "N° fournisseur :"
|
||||
#. module: base_usability
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__name
|
||||
msgid "Tag Name"
|
||||
msgstr "Libellé de l'étiquette"
|
||||
msgstr "Nom de l'étiquette"
|
||||
|
||||
#. module: base_usability
|
||||
#: code:addons/base_usability/models/res_company.py:0
|
||||
@@ -217,14 +167,6 @@ msgstr "Libellé de l'étiquette"
|
||||
msgid "Tel:"
|
||||
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
|
||||
#: model:ir.model,name:base_usability.model_res_users
|
||||
msgid "Users"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, models, _
|
||||
from odoo.tools.misc import format_amount
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
@@ -40,78 +39,32 @@ class ResCompany(models.Model):
|
||||
'value': self.phone,
|
||||
# http://www.fileformat.info/info/unicode/char/1f4de/index.htm
|
||||
'icon': '\U0001F4DE',
|
||||
'label': _('Tel:'),
|
||||
},
|
||||
'label': _('Tel:')},
|
||||
'email': {
|
||||
'value': self.email,
|
||||
# http://www.fileformat.info/info/unicode/char/2709/index.htm
|
||||
'icon': '\u2709',
|
||||
'label': _('E-mail:'),
|
||||
},
|
||||
'label': _('E-mail:')},
|
||||
'website': {
|
||||
'value': self.website,
|
||||
'icon': '\U0001f310',
|
||||
'label': _('Website:'),
|
||||
},
|
||||
'label': _('Website:')},
|
||||
'vat': {
|
||||
'value': self.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:'),
|
||||
}
|
||||
'label': _('VAT:')},
|
||||
}
|
||||
# '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
|
||||
|
||||
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):
|
||||
'''Method inherited in the module base_company_extension'''
|
||||
self.ensure_one()
|
||||
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
|
||||
def _display_report_header(
|
||||
self, line_details=None, icon=True, line_separator=' - '):
|
||||
self, line_details=[['phone', 'website'], ['vat']],
|
||||
icon=True, line_separator=' - '):
|
||||
self.ensure_one()
|
||||
if line_details is None:
|
||||
line_details = self._report_header_line_details()
|
||||
|
||||
res = ''
|
||||
address = self.partner_id._display_address(without_company=True)
|
||||
address = address.replace('\n', ' - ')
|
||||
|
||||
@@ -125,20 +125,6 @@ class ResPartner(models.Model):
|
||||
'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 = []
|
||||
for detail in details:
|
||||
if options.get(detail) and options[detail]['value']:
|
||||
|
||||
@@ -27,17 +27,6 @@
|
||||
</field>
|
||||
</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">
|
||||
<field name="name">base_usability.title.on.partner.simplified.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
<field name="bank_name" position="after">
|
||||
<field name="bank_id"/>
|
||||
</field>
|
||||
<field name="acc_holder_name" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
|
||||
# Copyright (C) 2016-2019 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': 'MRP Average Cost',
|
||||
'version': '14.0.1.0.0',
|
||||
'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
|
||||
'category': 'Manufactuing',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Update standard_price upon validation of a manufacturing order',
|
||||
@@ -12,16 +12,16 @@
|
||||
MRP Average Cost
|
||||
================
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['mrp'],
|
||||
'data': [
|
||||
'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',
|
||||
'views/mrp_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
|
||||
# Copyright (C) 2016-2019 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).
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.tools import float_compare
|
||||
import odoo.addons.decimal_precision as dp
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,19 +17,23 @@ class MrpBomLabourLine(models.Model):
|
||||
|
||||
bom_id = fields.Many2one(
|
||||
comodel_name='mrp.bom',
|
||||
string='Bill of Material',
|
||||
string='Labour Lines',
|
||||
ondelete='cascade')
|
||||
|
||||
labour_time = fields.Float(
|
||||
string='Labour Time',
|
||||
required=True,
|
||||
digits='Labour Hours',
|
||||
digits=dp.get_precision('Labour Hours'),
|
||||
help="Average labour time for the production of "
|
||||
"items of the BOM, in hours.")
|
||||
|
||||
labour_cost_profile_id = fields.Many2one(
|
||||
comodel_name='labour.cost.profile',
|
||||
string='Labour Cost Profile',
|
||||
required=True)
|
||||
note = fields.Text()
|
||||
|
||||
note = fields.Text(
|
||||
string='Note')
|
||||
|
||||
_sql_constraints = [(
|
||||
'labour_time_positive',
|
||||
@@ -38,26 +44,6 @@ class MrpBomLabourLine(models.Model):
|
||||
class MrpBom(models.Model):
|
||||
_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(
|
||||
'labour_line_ids.labour_time',
|
||||
'labour_line_ids.labour_cost_profile_id.hour_cost')
|
||||
@@ -84,77 +70,107 @@ class MrpBom(models.Model):
|
||||
bom.total_components_cost = comp_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
|
||||
def _phantom_update_product_standard_price(self):
|
||||
logger.info('Start to auto-update cost price from phantom boms')
|
||||
logger.info('Start to auto-update cost price from phantom bom')
|
||||
boms = self.search([('type', '=', 'phantom')])
|
||||
boms.manual_update_product_standard_price()
|
||||
logger.info('End of the auto-update cost price from phantom boms')
|
||||
boms.with_context(
|
||||
product_price_history_origin='Automatic update of 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):
|
||||
prec = self.env['decimal.precision'].precision_get(
|
||||
if 'product_price_history_origin' not in self._context:
|
||||
self = self.with_context(
|
||||
product_price_history_origin='Manual update from BOM')
|
||||
precision = self.env['decimal.precision'].precision_get(
|
||||
'Product Price')
|
||||
for bom in self:
|
||||
if bom.product_id:
|
||||
products = bom.product_id
|
||||
else:
|
||||
products = bom.product_tmpl_id.product_variant_ids
|
||||
for product in products:
|
||||
standard_price = product._compute_bom_price(bom)
|
||||
if float_compare(product.standard_price, standard_price, precision_digits=prec):
|
||||
product.write({'standard_price': standard_price})
|
||||
logger.info(
|
||||
'Cost price updated to %s on product %s',
|
||||
standard_price, product.display_name)
|
||||
wproduct = bom.product_id
|
||||
if not wproduct:
|
||||
wproduct = bom.product_tmpl_id
|
||||
if float_compare(
|
||||
wproduct.standard_price, bom.total_cost,
|
||||
precision_digits=precision):
|
||||
wproduct.with_context().write(
|
||||
{'standard_price': bom.total_cost})
|
||||
logger.info(
|
||||
'Cost price updated to %s on product %s',
|
||||
bom.total_cost, wproduct.display_name)
|
||||
return True
|
||||
|
||||
|
||||
class MrpBomLine(models.Model):
|
||||
_inherit = 'mrp.bom.line'
|
||||
|
||||
standard_price = fields.Float(related='product_id.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
|
||||
standard_price = fields.Float(
|
||||
related='product_id.standard_price',
|
||||
readonly=True,
|
||||
string='Standard Price')
|
||||
|
||||
|
||||
class LabourCostProfile(models.Model):
|
||||
_name = 'labour.cost.profile'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_inherit = ['mail.thread']
|
||||
_description = 'Labour Cost Profile'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True)
|
||||
track_visibility='onchange')
|
||||
|
||||
hour_cost = fields.Float(
|
||||
string='Cost per Hour',
|
||||
required=True,
|
||||
digits='Product Price',
|
||||
tracking=True,
|
||||
digits=dp.get_precision('Product Price'),
|
||||
track_visibility='onchange',
|
||||
help="Labour cost per hour per person in company currency")
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
comodel_name='res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
|
||||
company_currency_id = fields.Many2one(
|
||||
related='company_id.currency_id', store=True, string='Company Currency')
|
||||
related='company_id.currency_id',
|
||||
readonly=True,
|
||||
store=True,
|
||||
string='Company Currency')
|
||||
|
||||
@api.depends('name', 'hour_cost', 'company_currency_id.symbol')
|
||||
def name_get(self):
|
||||
res = []
|
||||
for record in self:
|
||||
res.append((record.id, '%s (%s %s)' % (
|
||||
res.append((record.id, u'%s (%s %s)' % (
|
||||
record.name, record.hour_cost,
|
||||
record.company_currency_id.symbol)))
|
||||
return res
|
||||
@@ -163,25 +179,93 @@ class LabourCostProfile(models.Model):
|
||||
class MrpProduction(models.Model):
|
||||
_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(
|
||||
related='company_id.currency_id', 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.")
|
||||
related='company_id.currency_id', readonly=True,
|
||||
string='Company Currency')
|
||||
|
||||
# Strategy for v14 : we write labor costs and bom's extra cost on the native field extra_cost
|
||||
# of mrp.production => it is automatically added by the code of mrp_account
|
||||
def compute_order_unit_cost(self):
|
||||
self.ensure_one()
|
||||
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
|
||||
|
||||
@api.depends('bom_id', 'product_id')
|
||||
def _compute_extra_cost(self):
|
||||
for prod in self:
|
||||
bom = prod.bom_id
|
||||
if bom and bom.type == 'normal':
|
||||
extra_cost_bom_qty_uom = bom.extra_cost + bom.total_labour_cost
|
||||
extra_cost_per_unit_in_prod_uom = 0
|
||||
qty_prod_uom = bom.product_uom_id._compute_quantity(bom.product_qty, prod.product_uom_id)
|
||||
if qty_prod_uom:
|
||||
extra_cost_per_unit_in_prod_uom = extra_cost_bom_qty_uom / qty_prod_uom
|
||||
prod.extra_cost = extra_cost_per_unit_in_prod_uom
|
||||
def post_inventory(self):
|
||||
'''This is the method where _action_done() is called on finished move
|
||||
So we write on 'price_unit' of the finished move and THEN we call
|
||||
super() which will call _action_done() which itself calls
|
||||
product_price_update_before_done()'''
|
||||
for order in self:
|
||||
if order.product_id.cost_method == 'average':
|
||||
unit_cost = order.compute_order_unit_cost()
|
||||
order.write({'unit_cost': unit_cost})
|
||||
logger.info('MO %s: unit_cost=%s', order.name, unit_cost)
|
||||
order.move_finished_ids.filtered(
|
||||
lambda x: x.product_id == order.product_id).write({
|
||||
'price_unit': unit_cost})
|
||||
return super(MrpProduction, self).post_inventory()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<record id="labour_cost_profile_rule" model="ir.rule">
|
||||
<field name="name">Labour Cost Profile multi-company</field>
|
||||
<field name="model_id" ref="model_labour_cost_profile"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2016-2024 Akretion (http://www.akretion.com/)
|
||||
Copyright (C) 2016-2019 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).
|
||||
-->
|
||||
@@ -13,25 +13,23 @@
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='miscellaneous']/group" position="inside">
|
||||
<group name="costs">
|
||||
<field name="total_components_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="total_labour_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="extra_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<label for="total_cost"/>
|
||||
<div>
|
||||
<field name="total_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"
|
||||
class="oe_inline"/>
|
||||
<button type="object" name="manual_update_product_standard_price"
|
||||
string="Update Cost Price of Product" class="oe_link"/>
|
||||
</div>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<field name="picking_type_id" position="after">
|
||||
<field name="total_components_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="total_labour_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="extra_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"/>
|
||||
<label for="total_cost"/>
|
||||
<div>
|
||||
<field name="total_cost" widget="monetary"
|
||||
options="{'currency_field': 'company_currency_id'}"
|
||||
class="oe_inline"/>
|
||||
<button type="object" name="manual_update_product_standard_price"
|
||||
string="Update Cost Price of Product" class="oe_link"/>
|
||||
</div>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
</field>
|
||||
<notebook position="inside">
|
||||
<page string="Labour" name="labour_lines">
|
||||
<group name="labour_lines_grp">
|
||||
@@ -119,10 +117,10 @@
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='miscellaneous']//field[@name='origin']/.." position="inside">
|
||||
<field name="extra_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="availability" position="after">
|
||||
<field name="unit_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}" attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{
|
||||
'name': 'MRP No Product Template Menu',
|
||||
'version': '14.0.1.0.0',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Manufacturing',
|
||||
'license': 'AGPL-3',
|
||||
'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>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['mrp', 'sale_purchase_no_product_template_menu'],
|
||||
'auto_install': True,
|
||||
'data': ['mrp_view.xml'],
|
||||
'installable': True,
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2016-2024 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2016-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).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="mrp.menu_mrp_product_form" model="ir.ui.menu">
|
||||
<field name="action" ref="product.product_normal_action"/>
|
||||
<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">
|
||||
<field name="action" ref="product_product_action_mrp"/>
|
||||
</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>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
'data': [
|
||||
'views/mrp_production.xml',
|
||||
'views/product_template.xml',
|
||||
'views/stock_move_line.xml',
|
||||
# 'report/mrp_report.xml' # TODO
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -3,23 +3,12 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_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
|
||||
@api.model
|
||||
def get_stock_move_sold_out_report(self, move):
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
<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"/>
|
||||
<field name="bom_id" position="after">
|
||||
<!-- no need to set readonly via attrs, the readonly status is in the field definition -->
|
||||
<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 name="location_src_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
</field>
|
||||
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='date_deadline']" position="after">
|
||||
<field name="date_start"/>
|
||||
@@ -41,20 +40,4 @@
|
||||
</field>
|
||||
</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>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?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>
|
||||
@@ -35,7 +35,6 @@ Akretion:
|
||||
"views/pos_config.xml",
|
||||
"views/product.xml",
|
||||
"views/pos_payment_method.xml",
|
||||
"views/stock_warehouse.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?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>
|
||||
@@ -10,4 +10,4 @@ class ProductTemplate(models.Model):
|
||||
|
||||
detailed_type = fields.Selection(selection_add=[
|
||||
('product', 'Storable Product')
|
||||
], ondelete={'product': 'set default'}, default='product')
|
||||
], ondelete={'product': 'set default'})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com)
|
||||
# Copyright 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).
|
||||
|
||||
{
|
||||
'name': 'Sale Down Payment',
|
||||
'version': '14.0.1.0.0',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Sales',
|
||||
'license': 'AGPL-3',
|
||||
'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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
This module targets B2B companies that don't want to generate a down payment invoice for an advanced payment.
|
||||
|
||||
@@ -25,13 +25,11 @@ This module has been written by Alexis de Lattre from Akretion
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['sale'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/account_payment_register_sale_view.xml',
|
||||
# 'wizard/account_bank_statement_sale_view.xml',
|
||||
# 'views/account_bank_statement.xml',
|
||||
'wizard/account_bank_statement_sale_view.xml',
|
||||
'views/account_bank_statement.xml',
|
||||
'views/sale.xml',
|
||||
'views/account_move_line.xml',
|
||||
'views/account_payment.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from . import sale
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import account_payment
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# 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
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com)
|
||||
# Copyright 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).
|
||||
|
||||
@@ -9,23 +9,24 @@ from odoo.exceptions import ValidationError
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
sale_id = fields.Many2one(
|
||||
'sale.order', string='Sale Order', check_company=True,
|
||||
domain="[('partner_invoice_id', 'child_of', partner_id), ('state', '!=', 'cancel'), ('invoice_status', '!=', 'invoiced'), ('company_id', '=', company_id)]")
|
||||
sale_id = fields.Many2one('sale.order', string='Sale Order')
|
||||
account_internal_type = fields.Selection(
|
||||
related='account_id.user_type_id.type', store=True,
|
||||
string='Account Internal Type')
|
||||
|
||||
@api.constrains('sale_id', 'account_id')
|
||||
def _sale_id_check(self):
|
||||
def sale_id_check(self):
|
||||
for line in self:
|
||||
if line.sale_id and line.account_internal_type != 'receivable':
|
||||
if line.sale_id and line.account_id.internal_type != 'receivable':
|
||||
raise ValidationError(_(
|
||||
"The account move line '%s' is linked to sale order '%s' "
|
||||
"but it uses account '%s' which is not a receivable "
|
||||
"account.")
|
||||
% (line.display_name,
|
||||
line.sale_id.display_name,
|
||||
% (line.name,
|
||||
line.sale_id.name,
|
||||
line.account_id.display_name))
|
||||
|
||||
@api.onchange('account_id')
|
||||
def sale_advance_payement_account_id_change(self):
|
||||
if self.sale_id and self.account_internal_type != 'receivable':
|
||||
if self.sale_id and self.account_id.user_type_id.type != 'receivable':
|
||||
self.sale_id = False
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com)
|
||||
# Copyright 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 fields, models
|
||||
from odoo.tools import float_round
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
@@ -10,24 +11,49 @@ class AccountPayment(models.Model):
|
||||
|
||||
sale_id = fields.Many2one('sale.order', string='Sale Order')
|
||||
|
||||
def _prepare_move_line_default_vals(self, write_off_line_vals=None):
|
||||
line_vals_list = super()._prepare_move_line_default_vals(
|
||||
write_off_line_vals=write_off_line_vals)
|
||||
# Add to the receivable/payable line
|
||||
def action_validate_invoice_payment(self):
|
||||
if self.sale_id:
|
||||
self.post()
|
||||
else:
|
||||
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 (
|
||||
self.sale_id and
|
||||
len(line_vals_list) >= 2 and
|
||||
line_vals_list[1].get('account_id') == self.destination_account_id.id):
|
||||
line_vals_list[1]['sale_id'] = self.sale_id.id
|
||||
return line_vals_list
|
||||
self._context.get('active_model') == 'sale.order' and
|
||||
self._context.get('active_id')):
|
||||
so = self.env['sale.order'].browse(self._context['active_id'])
|
||||
res.update({
|
||||
'amount': so.amount_total,
|
||||
'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 action_post(self):
|
||||
super().action_post()
|
||||
for pay in self:
|
||||
if pay.sale_id and pay.payment_type == 'inbound':
|
||||
pay._sale_down_payment_hook()
|
||||
|
||||
def _sale_down_payment_hook(self):
|
||||
# can be used for notifications
|
||||
# WAS on account.move.line on v12 ; is on account.payment on v14
|
||||
self.ensure_one()
|
||||
def _compute_payment_amount(self, invoices=None, currency=None):
|
||||
amount = super(AccountAbstractPayment, self)._compute_payment_amount(
|
||||
invoices=invoices, currency=currency)
|
||||
if self.sale_id:
|
||||
payment_currency = currency
|
||||
if not payment_currency:
|
||||
payment_currency = self.sale_id.currency_id
|
||||
amount = float_round(
|
||||
self.sale_id.amount_total - self.sale_id.amount_down_payment,
|
||||
precision_rounding=payment_currency.rounding)
|
||||
return amount
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com)
|
||||
# Copyright 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).
|
||||
|
||||
@@ -9,8 +9,8 @@ from odoo.tools import float_round
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
payment_ids = fields.One2many(
|
||||
'account.payment', 'sale_id', string='Advance Payments',
|
||||
payment_line_ids = fields.One2many(
|
||||
'account.move.line', 'sale_id', string='Advance Payments',
|
||||
readonly=True)
|
||||
amount_down_payment = fields.Monetary(
|
||||
compute='_compute_amount_down_payment', string='Down Payment Amount')
|
||||
@@ -19,20 +19,30 @@ class SaleOrder(models.Model):
|
||||
compute='_compute_amount_down_payment', string='Residual')
|
||||
|
||||
@api.depends(
|
||||
'payment_ids.amount', 'payment_ids.currency_id', 'payment_ids.date',
|
||||
'payment_ids.state', 'currency_id')
|
||||
'payment_line_ids.credit', 'payment_line_ids.debit',
|
||||
'payment_line_ids.amount_currency', 'payment_line_ids.currency_id',
|
||||
'payment_line_ids.date', 'currency_id')
|
||||
def _compute_amount_down_payment(self):
|
||||
for sale in self:
|
||||
down_payment = 0.0
|
||||
sale_currency = sale.currency_id
|
||||
prec_rounding = sale_currency.rounding or 0.01
|
||||
for payment in sale.payment_ids:
|
||||
if payment.payment_type == 'inbound' and payment.state == 'posted':
|
||||
down_payment += payment.currency_id._convert(
|
||||
payment.amount, sale_currency, sale.company_id,
|
||||
payment.date)
|
||||
sale_currency = sale.pricelist_id.currency_id
|
||||
if sale_currency == sale.company_id.currency_id:
|
||||
for pl in sale.payment_line_ids:
|
||||
down_payment -= pl.balance
|
||||
else:
|
||||
for pl in sale.payment_line_ids:
|
||||
if (
|
||||
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, precision_rounding=prec_rounding)
|
||||
down_payment, precision_rounding=sale.currency_id.rounding)
|
||||
sale.amount_down_payment = down_payment
|
||||
sale.amount_residual = float_round(
|
||||
sale.amount_total - down_payment, precision_rounding=prec_rounding)
|
||||
sale.amount_total - down_payment,
|
||||
precision_rounding=sale.currency_id.rounding)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019-2024 Akretion France (http://www.akretion.com/)
|
||||
Copyright 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).
|
||||
-->
|
||||
@@ -13,12 +13,10 @@
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='blocked']/.." position="after">
|
||||
<group string="Sale Order" name="sale" attrs="{'invisible': [('account_internal_type', '!=', 'receivable')]}">
|
||||
<field name="sale_id"/>
|
||||
<field name="account_internal_type" invisible="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<field name="invoice_id" 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')]"/>
|
||||
<field name="account_internal_type" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2018-2024 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2018 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).
|
||||
-->
|
||||
@@ -8,13 +8,13 @@
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_account_payment_form" model="ir.ui.view">
|
||||
<record id="view_account_payment_sale_form" model="ir.ui.view">
|
||||
<field name="name">account.payment.sale.form</field>
|
||||
<field name="model">account.payment</field>
|
||||
<field name="inherit_id" ref="account.view_account_payment_form"/>
|
||||
<field name="inherit_id" ref="account.view_account_payment_invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="destination_account_id" position="after">
|
||||
<field name="sale_id" attrs="{'invisible': [('is_internal_transfer', '=', True)]}"/>
|
||||
<field name="invoice_ids" position="after">
|
||||
<field name="sale_id" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2019-2024 Akretion France (http://www.akretion.com/)
|
||||
Copyright 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).
|
||||
-->
|
||||
@@ -8,6 +8,14 @@
|
||||
<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">
|
||||
<field name="name">advance_payment.sale.order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
@@ -15,7 +23,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="inside">
|
||||
<page name="advance_payment" string="Advance Payments">
|
||||
<field name="payment_ids" nolabel="1" colspan="2"/>
|
||||
<field name="payment_line_ids" nolabel="1"/>
|
||||
<field name="amount_residual" invisible="1"/>
|
||||
</page>
|
||||
</notebook>
|
||||
@@ -26,7 +34,7 @@
|
||||
<field name="amount_down_payment" nolabel="1" class="oe_subtotal_footer_separator"/>
|
||||
</field>
|
||||
<button name="action_cancel" position="before">
|
||||
<button type="action" name="%(sale_down_payment.account_payment_register_sale_action)d" string="Register Payment" attrs="{'invisible': ['|', ('amount_residual', '<=', 0), ('invoice_status', '=', 'invoiced')]}"/>
|
||||
<button type="action" name="%(sale_account_payment_action)d" string="Register Payment" attrs="{'invisible': ['|', ('amount_residual', '<=', 0), ('invoice_status', '=', 'invoiced')]}"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
from . import account_payment_register_sale
|
||||
# from . import account_bank_statement_sale
|
||||
from . import account_bank_statement_sale
|
||||
|
||||
@@ -64,9 +64,7 @@ class AccountBankStatementSale(models.TransientModel):
|
||||
self.ensure_one()
|
||||
for line in self.line_ids:
|
||||
if line.move_line_id.sale_id != line.sale_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()
|
||||
line.move_line_id.sale_id = line.sale_id.id
|
||||
|
||||
|
||||
class AccountBankStatementSaleLine(models.TransientModel):
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# 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
|
||||
@@ -1,43 +0,0 @@
|
||||
<?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>
|
||||
@@ -1 +1,2 @@
|
||||
from . import models
|
||||
from . import sale
|
||||
from . import sale_report
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
|
||||
{
|
||||
"name": "Sale Margin No Onchange",
|
||||
"version": "14.0.1.0.0",
|
||||
"category": "Sales",
|
||||
"license": "AGPL-3",
|
||||
"summary": "Copy standard price on sale order line and compute margins",
|
||||
"description": """
|
||||
'name': 'Sale Margin No Onchange',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Sales',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Copy standard price on sale order line and compute margins',
|
||||
'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).
|
||||
|
||||
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
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
"author": "Akretion",
|
||||
"website": "http://www.akretion.com",
|
||||
"depends": ["sale"],
|
||||
"data": ["views/sale_view.xml"],
|
||||
"installable": True,
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['sale'],
|
||||
'data': ['sale_view.xml'],
|
||||
'installable': False,
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import sale
|
||||
from . import sale_report
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
|
||||
from odoo import api, fields, models
|
||||
import odoo.addons.decimal_precision as dp
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
@@ -15,23 +16,23 @@ class SaleOrderLine(models.Model):
|
||||
store=True, string='Company Currency')
|
||||
standard_price_company_currency = fields.Float(
|
||||
string='Cost Price in Company Currency', readonly=True,
|
||||
digits="Product Price",
|
||||
digits=dp.get_precision('Product Price'),
|
||||
help="Cost price in company currency in the unit of measure "
|
||||
"of the sale order line")
|
||||
standard_price_sale_currency = fields.Float(
|
||||
string='Cost Price in Sale Currency',
|
||||
string='Cost Price in Sale Currency', readonly=True,
|
||||
compute='_compute_margin', store=True,
|
||||
digits="Product Price",
|
||||
digits=dp.get_precision('Product Price'),
|
||||
help="Cost price in sale currency in the unit of measure "
|
||||
"of the sale order line")
|
||||
margin_sale_currency = fields.Monetary(
|
||||
string='Margin in Sale Currency', store=True,
|
||||
string='Margin in Sale Currency', readonly=True, store=True,
|
||||
compute='_compute_margin', currency_field='currency_id')
|
||||
margin_company_currency = fields.Monetary(
|
||||
string='Margin in Company Currency', store=True,
|
||||
string='Margin in Company Currency', readonly=True, store=True,
|
||||
compute='_compute_margin', currency_field='company_currency_id')
|
||||
margin_rate = fields.Float(
|
||||
string="Margin Rate", store=True,
|
||||
string="Margin Rate", readonly=True, store=True,
|
||||
compute='_compute_margin',
|
||||
digits=(16, 2), help="Margin rate in percentage of the sale price")
|
||||
|
||||
@@ -67,20 +68,19 @@ class SaleOrderLine(models.Model):
|
||||
line.margin_rate = margin_rate
|
||||
|
||||
# We want to copy standard_price on sale order line
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('product_id'):
|
||||
pp = self.env['product.product'].browse(vals['product_id'])
|
||||
std_price = pp.standard_price
|
||||
sale_uom_id = vals.get('product_uom')
|
||||
if sale_uom_id and sale_uom_id != pp.uom_id.id:
|
||||
sale_uom = self.env['uom.uom'].browse(sale_uom_id)
|
||||
# convert from product UoM to sale UoM
|
||||
std_price = pp.uom_id._compute_price(
|
||||
pp.standard_price, sale_uom)
|
||||
vals['standard_price_company_currency'] = std_price
|
||||
return super().create(vals_list)
|
||||
@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
|
||||
sale_uom_id = vals.get('product_uom')
|
||||
if sale_uom_id and sale_uom_id != pp.uom_id.id:
|
||||
sale_uom = self.env['uom.uom'].browse(sale_uom_id)
|
||||
# convert from product UoM to sale UoM
|
||||
std_price = pp.uom_id._compute_price(
|
||||
pp.standard_price, sale_uom)
|
||||
vals['standard_price_company_currency'] = std_price
|
||||
return super(SaleOrderLine, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
if not vals:
|
||||
@@ -101,7 +101,7 @@ class SaleOrderLine(models.Model):
|
||||
if sale_uom != pp.uom_id:
|
||||
std_price = pp.uom_id._compute_price(std_price, sale_uom)
|
||||
sol.write({'standard_price_company_currency': std_price})
|
||||
return super().write(vals)
|
||||
return super(SaleOrderLine, self).write(vals)
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
@@ -114,27 +114,21 @@ class SaleOrder(models.Model):
|
||||
margin_sale_currency = fields.Monetary(
|
||||
string='Margin in Sale Currency',
|
||||
currency_field='currency_id',
|
||||
compute='_compute_margin', store=True)
|
||||
readonly=True, compute='_compute_margin', store=True)
|
||||
margin_company_currency = fields.Monetary(
|
||||
string='Margin in Company Currency',
|
||||
currency_field='company_currency_id',
|
||||
compute='_compute_margin', store=True)
|
||||
readonly=True, compute='_compute_margin', store=True)
|
||||
|
||||
@api.depends(
|
||||
'order_line.margin_sale_currency',
|
||||
'order_line.margin_company_currency')
|
||||
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:
|
||||
order.margin_sale_currency = mapped_data.get(
|
||||
order.id, {}).get('margin_sale_currency')
|
||||
order.margin_company_currency = mapped_data.get(
|
||||
order.id, {}).get('margin_company_currency')
|
||||
margin_sale_cur = 0.0
|
||||
margin_comp_cur = 0.0
|
||||
for sol in order.order_line:
|
||||
margin_sale_cur += sol.margin_sale_currency
|
||||
margin_comp_cur += sol.margin_company_currency
|
||||
order.margin_sale_currency = margin_sale_cur
|
||||
order.margin_company_currency = margin_comp_cur
|
||||
@@ -20,8 +20,7 @@
|
||||
groups="account.group_account_user"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
</group>
|
||||
<xpath expr="//field[@name='order_line']/form/group/group//field[@name='analytic_tag_ids']/.." position="after">
|
||||
<group>
|
||||
<xpath expr="//field[@name='order_line']/form//field[@name='analytic_tag_ids']/.." position="after">
|
||||
<field name="standard_price_sale_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"/>
|
||||
@@ -32,7 +31,6 @@
|
||||
</div>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -1,2 +1 @@
|
||||
from . import sale_order
|
||||
from . import sale_report
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
@@ -49,14 +49,6 @@ entry and link it to product product -->
|
||||
<field name="context">{}</field>
|
||||
</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 -->
|
||||
<menuitem id="sale_config_product_template_menu" action="product.product_template_action"
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
'views/account_move.xml',
|
||||
'views/res_company.xml',
|
||||
"views/res_partner.xml",
|
||||
"views/sale_template.xml",
|
||||
'wizards/sale_invoice_discount_all_lines_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
|
||||
@@ -135,29 +135,3 @@ class SaleOrderLine(models.Model):
|
||||
if no_product_code_param and no_product_code_param == 'True':
|
||||
product = product.with_context(display_default_code=False)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -59,10 +59,6 @@
|
||||
</field>
|
||||
<field name="state" position="attributes">
|
||||
<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 name="partner_id" position="after">
|
||||
<field name="client_order_ref" optional="show"/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?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" /> -> <t t-esc="float(qty_delivered)"/><br/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
</odoo>
|
||||
1
service_line_qty_update_base/__init__.py
Normal file
1
service_line_qty_update_base/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import wizard
|
||||
18
service_line_qty_update_base/__manifest__.py
Normal file
18
service_line_qty_update_base/__manifest__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# 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,
|
||||
}
|
||||
1
service_line_qty_update_base/wizard/__init__.py
Normal file
1
service_line_qty_update_base/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import service_qty_update
|
||||
70
service_line_qty_update_base/wizard/service_qty_update.py
Normal file
70
service_line_qty_update_base/wizard/service_qty_update.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# 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
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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>
|
||||
2
service_line_qty_update_purchase/__init__.py
Normal file
2
service_line_qty_update_purchase/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
22
service_line_qty_update_purchase/__manifest__.py
Normal file
22
service_line_qty_update_purchase/__manifest__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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,
|
||||
}
|
||||
1
service_line_qty_update_purchase/models/__init__.py
Normal file
1
service_line_qty_update_purchase/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import purchase_order
|
||||
21
service_line_qty_update_purchase/models/purchase_order.py
Normal file
21
service_line_qty_update_purchase/models/purchase_order.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
27
service_line_qty_update_purchase/views/purchase_order.xml
Normal file
27
service_line_qty_update_purchase/views/purchase_order.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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>
|
||||
1
service_line_qty_update_purchase/wizard/__init__.py
Normal file
1
service_line_qty_update_purchase/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import service_qty_update
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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()
|
||||
2
service_line_qty_update_sale/__init__.py
Normal file
2
service_line_qty_update_sale/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
22
service_line_qty_update_sale/__manifest__.py
Normal file
22
service_line_qty_update_sale/__manifest__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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,
|
||||
}
|
||||
1
service_line_qty_update_sale/models/__init__.py
Normal file
1
service_line_qty_update_sale/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import sale_order
|
||||
21
service_line_qty_update_sale/models/sale_order.py
Normal file
21
service_line_qty_update_sale/models/sale_order.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
26
service_line_qty_update_sale/views/sale_order.xml
Normal file
26
service_line_qty_update_sale/views/sale_order.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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>
|
||||
1
service_line_qty_update_sale/wizard/__init__.py
Normal file
1
service_line_qty_update_sale/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import service_qty_update
|
||||
62
service_line_qty_update_sale/wizard/service_qty_update.py
Normal file
62
service_line_qty_update_sale/wizard/service_qty_update.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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()
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import models
|
||||
from .hooks import post_init_hook
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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",
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import stock_location
|
||||
from . import stock_warehouse
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1 +0,0 @@
|
||||
from . import test_stock_location_simple
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,90 +0,0 @@
|
||||
<?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>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{
|
||||
'name': 'Stock No Product Template Menu',
|
||||
'version': '14.0.1.0.0',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Stock',
|
||||
'license': 'AGPL-3',
|
||||
'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>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['stock'],
|
||||
'data': ['view.xml'],
|
||||
'installable': True,
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
<record id="stock.menu_product_variant_config_stock" model="ir.ui.menu">
|
||||
<field name="action" ref="product.product_normal_action"/>
|
||||
</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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user