Compare commits
76 Commits
14-fix-mig
...
14.0-sale_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e0357a8fc | ||
|
|
37d41d20b7 | ||
|
|
4b2dcb4a86 | ||
|
|
29b8ebb779 | ||
|
|
c33835957d | ||
|
|
73e700d2d2 | ||
|
|
5144b039a5 | ||
|
|
0e237d26cb | ||
|
|
b252bdff34 | ||
|
|
550704288d | ||
|
|
320cfff25f | ||
|
|
26a7a42e8c | ||
|
|
3ca4553eb5 | ||
|
|
309d466374 | ||
|
|
61f43e5d02 | ||
|
|
6d847dcbe9 | ||
|
|
85b4cc25eb | ||
|
|
901a0e5816 | ||
|
|
569b3fea1a | ||
|
|
60589b1743 | ||
|
|
5ce7ed3fe7 | ||
|
|
ab3562a737 | ||
|
|
28c6aca721 | ||
|
|
b353bb14a5 | ||
|
|
282e7142db | ||
|
|
763928c286 | ||
|
|
4774b879fa | ||
|
|
18a5c22160 | ||
|
|
8f87df3f3d | ||
|
|
6af447974c | ||
|
|
46f2e0e01d | ||
|
|
ad8edd00d2 | ||
|
|
97d57e40eb | ||
|
|
fcf67f4fd9 | ||
|
|
89f81053cd | ||
|
|
b867dd6d01 | ||
|
|
f2172a5f06 | ||
|
|
e81e8e5a93 | ||
|
|
a7b0210a90 | ||
|
|
cd30298f88 | ||
|
|
5039e56417 | ||
|
|
3c338c9c78 | ||
|
|
2c87670281 | ||
|
|
59e34d0166 | ||
|
|
0e87b385d4 | ||
|
|
4a434d69f9 | ||
|
|
bade674c41 | ||
|
|
def89c0a5d | ||
|
|
88b12d86a2 | ||
|
|
e3c55047b3 | ||
|
|
b701586edc | ||
|
|
aae3ac2898 | ||
|
|
8a2e23cff5 | ||
|
|
0781cb7ba7 | ||
|
|
a7022e899f | ||
|
|
068509066d | ||
|
|
e594d15417 | ||
|
|
4121d8466d | ||
|
|
6df9756479 | ||
|
|
0331cc8eda | ||
|
|
54bc3d8af8 | ||
|
|
15edfe5603 | ||
|
|
8bf2116630 | ||
|
|
6b5282994a | ||
|
|
993aab7d18 | ||
|
|
73137ee1fb | ||
|
|
c4ec388380 | ||
|
|
8afcd49bc3 | ||
|
|
1e8f00d4e1 | ||
|
|
061efb2197 | ||
|
|
e698746dd1 | ||
|
|
d2bc7be9fe | ||
|
|
5f6b731e50 | ||
|
|
6c81ade7d0 | ||
|
|
209de4f6db | ||
|
|
2d3a792ce8 |
@@ -1,2 +1 @@
|
||||
from . import account_invoice
|
||||
from . import account_invoice_report
|
||||
from . import models
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{
|
||||
'name': 'Account Invoice Margin',
|
||||
'version': '12.0.1.0.0',
|
||||
'version': '14.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': 'http://www.akretion.com',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['account'],
|
||||
'data': [
|
||||
'account_invoice_view.xml',
|
||||
'views/account_move.xml',
|
||||
],
|
||||
'installable': False,
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
# Copyright 2015-2019 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
import odoo.addons.decimal_precision as dp
|
||||
|
||||
|
||||
class AccountInvoiceLine(models.Model):
|
||||
_inherit = 'account.invoice.line'
|
||||
|
||||
standard_price_company_currency = fields.Float(
|
||||
string='Cost Price in Company Currency', readonly=True,
|
||||
digits=dp.get_precision('Product Price'),
|
||||
help="Cost price in company currency in the unit of measure "
|
||||
"of the invoice line (which may be different from the unit "
|
||||
"of measure of the product).")
|
||||
standard_price_invoice_currency = fields.Float(
|
||||
string='Cost Price in Invoice Currency', readonly=True,
|
||||
compute='_compute_margin', store=True,
|
||||
digits=dp.get_precision('Product Price'),
|
||||
help="Cost price in invoice currency in the unit of measure "
|
||||
"of the invoice line")
|
||||
margin_invoice_currency = fields.Monetary(
|
||||
string='Margin in Invoice Currency', readonly=True, store=True,
|
||||
compute='_compute_margin', currency_field='currency_id')
|
||||
margin_company_currency = fields.Monetary(
|
||||
string='Margin in Company Currency', readonly=True, store=True,
|
||||
compute='_compute_margin', currency_field='company_currency_id')
|
||||
margin_rate = fields.Float(
|
||||
string="Margin Rate", readonly=True, store=True,
|
||||
compute='_compute_margin',
|
||||
digits=(16, 2), help="Margin rate in percentage of the sale price")
|
||||
|
||||
@api.depends(
|
||||
'standard_price_company_currency', 'invoice_id.currency_id',
|
||||
'invoice_id.type', 'invoice_id.company_id',
|
||||
'invoice_id.date_invoice', 'quantity', 'price_subtotal')
|
||||
def _compute_margin(self):
|
||||
for il in self:
|
||||
standard_price_inv_cur = 0.0
|
||||
margin_inv_cur = 0.0
|
||||
margin_comp_cur = 0.0
|
||||
margin_rate = 0.0
|
||||
inv = il.invoice_id
|
||||
if inv and inv.type in ('out_invoice', 'out_refund'):
|
||||
# it works in _get_current_rate
|
||||
# even if we set date = False in context
|
||||
# standard_price_inv_cur is in the UoM of the invoice line
|
||||
date = inv._get_currency_rate_date() or\
|
||||
fields.Date.context_today(self)
|
||||
company = inv.company_id
|
||||
company_currency = company.currency_id
|
||||
standard_price_inv_cur =\
|
||||
company_currency._convert(
|
||||
il.standard_price_company_currency,
|
||||
inv.currency_id, company, date)
|
||||
margin_inv_cur =\
|
||||
il.price_subtotal - il.quantity * standard_price_inv_cur
|
||||
margin_comp_cur = inv.currency_id._convert(
|
||||
margin_inv_cur, company_currency, company, date)
|
||||
if il.price_subtotal:
|
||||
margin_rate = 100 * margin_inv_cur / il.price_subtotal
|
||||
# for a refund, margin should be negative
|
||||
# but margin rate should stay positive
|
||||
if inv.type == 'out_refund':
|
||||
margin_inv_cur *= -1
|
||||
margin_comp_cur *= -1
|
||||
il.standard_price_invoice_currency = standard_price_inv_cur
|
||||
il.margin_invoice_currency = margin_inv_cur
|
||||
il.margin_company_currency = margin_comp_cur
|
||||
il.margin_rate = margin_rate
|
||||
|
||||
# We want to copy standard_price on invoice line for customer
|
||||
# invoice/refunds. We can't do that via on_change of product_id,
|
||||
# because it is not always played when invoice is created from code
|
||||
# => we inherit write/create
|
||||
# We write standard_price_company_currency even on supplier invoice/refunds
|
||||
# because we don't have access to the 'type' of the invoice
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('product_id'):
|
||||
pp = self.env['product.product'].browse(vals['product_id'])
|
||||
std_price = pp.standard_price
|
||||
inv_uom_id = vals.get('uom_id')
|
||||
if inv_uom_id and inv_uom_id != pp.uom_id.id:
|
||||
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
|
||||
std_price = pp.uom_id._compute_price(
|
||||
std_price, inv_uom)
|
||||
vals['standard_price_company_currency'] = std_price
|
||||
return super(AccountInvoiceLine, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
if not vals:
|
||||
vals = {}
|
||||
if 'product_id' in vals or 'uom_id' in vals:
|
||||
for il in self:
|
||||
if 'product_id' in vals:
|
||||
if vals.get('product_id'):
|
||||
pp = self.env['product.product'].browse(
|
||||
vals['product_id'])
|
||||
else:
|
||||
pp = False
|
||||
else:
|
||||
pp = il.product_id or False
|
||||
# uom_id is NOT a required field
|
||||
if 'uom_id' in vals:
|
||||
if vals.get('uom_id'):
|
||||
inv_uom = self.env['uom.uom'].browse(
|
||||
vals['uom_id'])
|
||||
else:
|
||||
inv_uom = False
|
||||
else:
|
||||
inv_uom = il.uom_id or False
|
||||
std_price = 0.0
|
||||
if pp:
|
||||
std_price = pp.standard_price
|
||||
if inv_uom and inv_uom != pp.uom_id:
|
||||
std_price = pp.uom_id._compute_price(
|
||||
std_price, inv_uom)
|
||||
il.write({'standard_price_company_currency': std_price})
|
||||
return super(AccountInvoiceLine, self).write(vals)
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
margin_invoice_currency = fields.Monetary(
|
||||
string='Margin in Invoice Currency',
|
||||
compute='_compute_margin', store=True, readonly=True,
|
||||
currency_field='currency_id')
|
||||
margin_company_currency = fields.Monetary(
|
||||
string='Margin in Company Currency',
|
||||
compute='_compute_margin', store=True, readonly=True,
|
||||
currency_field='company_currency_id')
|
||||
|
||||
@api.depends(
|
||||
'type',
|
||||
'invoice_line_ids.margin_invoice_currency',
|
||||
'invoice_line_ids.margin_company_currency')
|
||||
def _compute_margin(self):
|
||||
res = self.env['account.invoice.line'].read_group(
|
||||
[('invoice_id', 'in', self.ids)],
|
||||
['invoice_id', 'margin_invoice_currency',
|
||||
'margin_company_currency'],
|
||||
['invoice_id'])
|
||||
for re in res:
|
||||
if re['invoice_id']:
|
||||
inv = self.browse(re['invoice_id'][0])
|
||||
if inv.type in ('out_invoice', 'out_refund'):
|
||||
inv.margin_invoice_currency = re['margin_invoice_currency']
|
||||
inv.margin_company_currency = re['margin_company_currency']
|
||||
@@ -1,60 +0,0 @@
|
||||
# Copyright 2018-2019 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountInvoiceReport(models.Model):
|
||||
_inherit = 'account.invoice.report'
|
||||
|
||||
margin = fields.Float(string='Margin', readonly=True)
|
||||
# why digits=0 ??? Why is it like that in the native "account" module
|
||||
user_currency_margin = fields.Float(
|
||||
string="Margin", compute='_compute_user_currency_margin', digits=0)
|
||||
|
||||
_depends = {
|
||||
'account.invoice': [
|
||||
'account_id', 'amount_total_company_signed',
|
||||
'commercial_partner_id', 'company_id',
|
||||
'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id',
|
||||
'journal_id', 'number', 'partner_bank_id', 'partner_id',
|
||||
'payment_term_id', 'residual', 'state', 'type', 'user_id',
|
||||
],
|
||||
'account.invoice.line': [
|
||||
'account_id', 'invoice_id', 'price_subtotal', 'product_id',
|
||||
'quantity', 'uom_id', 'account_analytic_id',
|
||||
'margin_company_currency',
|
||||
],
|
||||
'product.product': ['product_tmpl_id'],
|
||||
'product.template': ['categ_id'],
|
||||
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
|
||||
'res.currency.rate': ['currency_id', 'name'],
|
||||
'res.partner': ['country_id'],
|
||||
}
|
||||
|
||||
@api.depends('currency_id', 'date', 'margin')
|
||||
def _compute_user_currency_margin(self):
|
||||
user_currency = self.env.user.company_id.currency_id
|
||||
currency_rate = self.env['res.currency.rate'].search([
|
||||
('rate', '=', 1),
|
||||
'|',
|
||||
('company_id', '=', self.env.user.company_id.id),
|
||||
('company_id', '=', False)], limit=1)
|
||||
base_currency = currency_rate.currency_id
|
||||
for record in self:
|
||||
date = record.date or fields.Date.today()
|
||||
company = record.company_id
|
||||
record.user_currency_margin = base_currency._convert(
|
||||
record.margin, user_currency, company, date)
|
||||
|
||||
# TODO check for refunds
|
||||
def _sub_select(self):
|
||||
select_str = super(AccountInvoiceReport, self)._sub_select()
|
||||
select_str += ", SUM(ail.margin_company_currency) AS margin"
|
||||
return select_str
|
||||
|
||||
def _select(self):
|
||||
select_str = super(AccountInvoiceReport, self)._select()
|
||||
select_str += ", sub.margin AS margin"
|
||||
return select_str
|
||||
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
© 2015-2017 Akretion (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_invoice_line_form" model="ir.ui.view">
|
||||
<field name="name">margin.account.invoice.line.form</field>
|
||||
<field name="model">account.invoice.line</field>
|
||||
<field name="inherit_id" ref="account.view_invoice_line_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='analytic_tag_ids']/.." position="inside">
|
||||
<field name="standard_price_company_currency"
|
||||
string="Cost Price in Comp. Cur."
|
||||
groups="base.group_no_one"/>
|
||||
<field name="standard_price_invoice_currency"
|
||||
string="Cost Price in Inv. Cur."
|
||||
groups="base.group_no_one"/>
|
||||
<field name="margin_invoice_currency"
|
||||
string="Margin in Inv. Cur."
|
||||
groups="base.group_no_one"/>
|
||||
<field name="margin_company_currency"
|
||||
string="Margin in Comp. Cur."
|
||||
groups="base.group_no_one"/>
|
||||
<label for="margin_rate" groups="base.group_no_one"/>
|
||||
<div name="margin_rate" groups="base.group_no_one">
|
||||
<field name="margin_rate" class="oe_inline"/> %
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="invoice_form" model="ir.ui.view">
|
||||
<field name="name">margin.account.invoice.form</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="inherit_id" ref="account.invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="move_id" position="after">
|
||||
<field name="margin_invoice_currency"
|
||||
string="Margin in Inv. Cur." groups="base.group_no_one"/>
|
||||
<field name="margin_company_currency"
|
||||
string="Margin in Comp. Cur." groups="base.group_no_one"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
2
account_invoice_margin/models/__init__.py
Normal file
2
account_invoice_margin/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import account_move
|
||||
from . import account_invoice_report
|
||||
36
account_invoice_margin/models/account_invoice_report.py
Normal file
36
account_invoice_margin/models/account_invoice_report.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Copyright 2018-2019 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountInvoiceReport(models.Model):
|
||||
_inherit = 'account.invoice.report'
|
||||
|
||||
margin = fields.Float(string='Margin', readonly=True)
|
||||
|
||||
# added margin_company_currency on account.move.line
|
||||
_depends = {
|
||||
'account.move': [
|
||||
'name', 'state', 'move_type', 'partner_id', 'invoice_user_id', 'fiscal_position_id',
|
||||
'invoice_date', 'invoice_date_due', 'invoice_payment_term_id', 'partner_bank_id',
|
||||
],
|
||||
'account.move.line': [
|
||||
'quantity', 'price_subtotal', 'amount_residual', 'balance', 'amount_currency',
|
||||
'move_id', 'product_id', 'product_uom_id', 'account_id', 'analytic_account_id',
|
||||
'journal_id', 'company_id', 'currency_id', 'partner_id',
|
||||
'margin_company_currency',
|
||||
],
|
||||
'product.product': ['product_tmpl_id'],
|
||||
'product.template': ['categ_id'],
|
||||
'uom.uom': ['category_id', 'factor', 'name', 'uom_type'],
|
||||
'res.currency.rate': ['currency_id', 'name'],
|
||||
'res.partner': ['country_id'],
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _select(self):
|
||||
select_str = super()._select()
|
||||
select_str += ", line.margin_company_currency * currency_table.rate AS margin"
|
||||
return select_str
|
||||
155
account_invoice_margin/models/account_move.py
Normal file
155
account_invoice_margin/models/account_move.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# Copyright 2015-2021 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
standard_price_company_currency = fields.Float(
|
||||
string='Unit Cost Price in Company Currency', readonly=True,
|
||||
digits='Product Price',
|
||||
help="Unit Cost price in company currency in the unit of measure "
|
||||
"of the invoice line (which may be different from the unit "
|
||||
"of measure of the product).")
|
||||
standard_price_invoice_currency = fields.Float(
|
||||
string='Unit Cost Price in Invoice Currency',
|
||||
compute='_compute_margin', store=True, digits='Product Price',
|
||||
help="Unit Cost price in invoice currency in the unit of measure "
|
||||
"of the invoice line.")
|
||||
margin_invoice_currency = fields.Monetary(
|
||||
string='Margin in Invoice Currency', store=True,
|
||||
compute='_compute_margin', currency_field='currency_id')
|
||||
margin_company_currency = fields.Monetary(
|
||||
string='Margin in Company Currency', store=True,
|
||||
compute='_compute_margin', currency_field='company_currency_id')
|
||||
margin_rate = fields.Float(
|
||||
string="Margin Rate", readonly=True, store=True,
|
||||
compute='_compute_margin',
|
||||
digits=(16, 2), help="Margin rate in percentage of the sale price")
|
||||
|
||||
@api.depends(
|
||||
'standard_price_company_currency', 'move_id.currency_id',
|
||||
'move_id.move_type', 'move_id.company_id',
|
||||
'move_id.invoice_date', 'quantity', 'price_subtotal')
|
||||
def _compute_margin(self):
|
||||
for ml in self:
|
||||
standard_price_inv_cur = 0.0
|
||||
margin_inv_cur = 0.0
|
||||
margin_comp_cur = 0.0
|
||||
margin_rate = 0.0
|
||||
move = ml.move_id
|
||||
if move.move_type and move.move_type in ('out_invoice', 'out_refund'):
|
||||
# it works in _get_current_rate
|
||||
# even if we set date = False in context
|
||||
# standard_price_inv_cur is in the UoM of the invoice line
|
||||
date = move.date or fields.Date.context_today(self)
|
||||
company = move.company_id
|
||||
company_currency = company.currency_id
|
||||
standard_price_inv_cur =\
|
||||
company_currency._convert(
|
||||
ml.standard_price_company_currency,
|
||||
ml.currency_id, company, date)
|
||||
margin_inv_cur =\
|
||||
ml.price_subtotal - ml.quantity * standard_price_inv_cur
|
||||
margin_comp_cur = move.currency_id._convert(
|
||||
margin_inv_cur, company_currency, company, date)
|
||||
if ml.price_subtotal:
|
||||
margin_rate = 100 * margin_inv_cur / ml.price_subtotal
|
||||
# for a refund, margin should be negative
|
||||
# but margin rate should stay positive
|
||||
if move.move_type == 'out_refund':
|
||||
margin_inv_cur *= -1
|
||||
margin_comp_cur *= -1
|
||||
ml.standard_price_invoice_currency = standard_price_inv_cur
|
||||
ml.margin_invoice_currency = margin_inv_cur
|
||||
ml.margin_company_currency = margin_comp_cur
|
||||
ml.margin_rate = margin_rate
|
||||
|
||||
# We want to copy standard_price on invoice line for customer
|
||||
# invoice/refunds. We can't do that via on_change of product_id,
|
||||
# because it is not always played when invoice is created from code
|
||||
# => we inherit write/create
|
||||
# We write standard_price_company_currency even on supplier invoice/refunds
|
||||
# because we don't have access to the 'type' of the invoice
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('product_id') and not vals.get('display_type'):
|
||||
pp = self.env['product.product'].browse(vals['product_id'])
|
||||
std_price = pp.standard_price
|
||||
inv_uom_id = vals.get('product_uom_id')
|
||||
if inv_uom_id and inv_uom_id != pp.uom_id.id:
|
||||
inv_uom = self.env['uom.uom'].browse(inv_uom_id)
|
||||
std_price = pp.uom_id._compute_price(
|
||||
std_price, inv_uom)
|
||||
vals['standard_price_company_currency'] = std_price
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if not vals:
|
||||
vals = {}
|
||||
if 'product_id' in vals or 'product_uom_id' in vals:
|
||||
for il in self:
|
||||
if 'product_id' in vals:
|
||||
if vals.get('product_id'):
|
||||
pp = self.env['product.product'].browse(
|
||||
vals['product_id'])
|
||||
else:
|
||||
pp = False
|
||||
else:
|
||||
pp = il.product_id or False
|
||||
# uom_id is NOT a required field
|
||||
if 'product_uom_id' in vals:
|
||||
if vals.get('product_uom_id'):
|
||||
inv_uom = self.env['uom.uom'].browse(
|
||||
vals['product_uom_id'])
|
||||
else:
|
||||
inv_uom = False
|
||||
else:
|
||||
inv_uom = il.uom_id or False
|
||||
std_price = 0.0
|
||||
if pp:
|
||||
std_price = pp.standard_price
|
||||
if inv_uom and inv_uom != pp.uom_id:
|
||||
std_price = pp.uom_id._compute_price(
|
||||
std_price, inv_uom)
|
||||
il.write({'standard_price_company_currency': std_price})
|
||||
return super().write(vals)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
margin_invoice_currency = fields.Monetary(
|
||||
string='Margin in Invoice Currency',
|
||||
compute='_compute_margin', store=True,
|
||||
currency_field='currency_id')
|
||||
margin_company_currency = fields.Monetary(
|
||||
string='Margin in Company Currency',
|
||||
compute='_compute_margin', store=True,
|
||||
currency_field='company_currency_id')
|
||||
|
||||
@api.depends(
|
||||
'move_type',
|
||||
'invoice_line_ids.margin_invoice_currency',
|
||||
'invoice_line_ids.margin_company_currency')
|
||||
def _compute_margin(self):
|
||||
rg_res = self.env['account.move.line'].read_group(
|
||||
[
|
||||
('move_id', 'in', self.ids),
|
||||
('display_type', '=', False),
|
||||
('exclude_from_invoice_tab', '=', False),
|
||||
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
|
||||
],
|
||||
['move_id', 'margin_invoice_currency:sum', 'margin_company_currency:sum'],
|
||||
['move_id'])
|
||||
mapped_data = dict([(x['move_id'][0], {
|
||||
'margin_invoice_currency': x['margin_invoice_currency'],
|
||||
'margin_company_currency': x['margin_company_currency'],
|
||||
}) for x in rg_res])
|
||||
for move in self:
|
||||
move.margin_invoice_currency = mapped_data.get(move.id, {}).get('margin_invoice_currency')
|
||||
move.margin_company_currency = mapped_data.get(move.id, {}).get('margin_company_currency')
|
||||
55
account_invoice_margin/views/account_move.xml
Normal file
55
account_invoice_margin/views/account_move.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2015-2024 Akretion (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_move_form" model="ir.ui.view">
|
||||
<field name="name">margin.account.move.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="sale_info_group" position="inside">
|
||||
<field name="margin_invoice_currency"
|
||||
groups="base.group_no_one"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<field name="margin_company_currency"
|
||||
groups="base.group_no_one"
|
||||
attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
</group>
|
||||
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='price_total']" position="after">
|
||||
<field name="standard_price_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<field name="margin_invoice_currency" optional="hide" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<field name="margin_rate" optional="hide" string="Margin Rate (%)" attrs="{'column_invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='invoice_line_ids']/form//field[@name='price_total']/.." position="inside">
|
||||
<field name="standard_price_company_currency"
|
||||
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<field name="standard_price_invoice_currency"
|
||||
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<field name="margin_invoice_currency"
|
||||
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<field name="margin_company_currency"
|
||||
groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<label for="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}"/>
|
||||
<div name="margin_rate" groups="base.group_no_one" attrs="{'invisible': [('parent.move_type', 'not in', ('out_invoice', 'out_refund'))]}">
|
||||
<field name="margin_rate" class="oe_inline"/> %
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_invoice_tree" model="ir.ui.view">
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_invoice_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="amount_residual_signed" position="after">
|
||||
<field name="margin_company_currency" optional="hide" sum="1" invisible="context.get('default_move_type') not in ('out_invoice', 'out_refund')" string="Margin"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -11,7 +11,8 @@
|
||||
<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" states="posted" groups="account.group_account_invoice"/>
|
||||
<button name="prepare_update_wizard" type="object" string="Update Invoice" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', '=', 'entry')]}"/>
|
||||
<button name="prepare_update_wizard" type="object" string="Update Entry" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', 'posted'), ('move_type', '!=', 'entry')]}"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -21,6 +21,7 @@ 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')
|
||||
@@ -30,7 +31,7 @@ class AccountMoveUpdate(models.TransientModel):
|
||||
@api.model
|
||||
def _simple_fields2update(self):
|
||||
'''List boolean, date, datetime, char, text fields'''
|
||||
return ['ref', 'invoice_origin']
|
||||
return ['ref', 'invoice_origin', 'invoice_date']
|
||||
|
||||
@api.model
|
||||
def _m2o_fields2update(self):
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
<field name="move_type" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="partner_id" invisible="1"/>
|
||||
<field string="Bill Reference" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="ref"/>
|
||||
<field string="Bill Date" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="invoice_date"/>
|
||||
<field string="Supplier Bill Reference" attrs="{'invisible': [('move_type', 'not in', ('in_invoice', 'in_refund'))]}" name="ref"/>
|
||||
<field string="Customer Reference" attrs="{'invisible': [('move_type', 'not in', ('out_invoice', 'out_refund'))]}" name="ref"/>
|
||||
<field name="invoice_origin"/>
|
||||
<field string="Ref" attrs="{'invisible': [('move_type', '!=', 'entry')]}" name="ref"/>
|
||||
<field name="invoice_origin" attrs="{'invisible': [('move_type', '=', 'entry')]}"/>
|
||||
<!-- update of payment term is broken -->
|
||||
<!-- <field name="invoice_payment_term_id" widget="selection"/>-->
|
||||
<field name="partner_bank_id"/>
|
||||
<field name="user_id" options="{'no_open': True, 'no_create': True, 'no_create_edit': True}"/>
|
||||
<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')]}"/>
|
||||
</group>
|
||||
<group name="lines">
|
||||
<field name="line_ids" nolabel="1" widget="section_and_note_one2many">
|
||||
@@ -30,8 +32,8 @@
|
||||
<field name="display_type" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)]}"/>
|
||||
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)]}"/>
|
||||
<field name="quantity" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
|
||||
<field name="price_subtotal" attrs="{'invisible': [('display_type', '!=', False)], 'column_invisible': [('parent.move_type', '=', 'entry')]}"/>
|
||||
<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>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
'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',
|
||||
|
||||
@@ -43,13 +43,6 @@ class AccountBankStatement(models.Model):
|
||||
res.append((statement.id, name))
|
||||
return res
|
||||
|
||||
def button_reopen(self):
|
||||
self = self.with_context(skip_undo_reconciliation=True)
|
||||
return super().button_reopen()
|
||||
|
||||
def button_undo_reconciliation(self):
|
||||
self.line_ids.button_undo_reconciliation()
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = 'account.bank.statement.line'
|
||||
@@ -96,9 +89,3 @@ class AccountBankStatementLine(models.Model):
|
||||
'res_id': self.move_id.id,
|
||||
})
|
||||
return action
|
||||
|
||||
def button_undo_reconciliation(self):
|
||||
if self._context.get("skip_undo_reconciliation"):
|
||||
return
|
||||
else:
|
||||
return super().button_undo_reconciliation()
|
||||
|
||||
@@ -6,10 +6,12 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.tools.misc import format_date
|
||||
from odoo.tools.safe_eval import safe_eval, time
|
||||
from collections import defaultdict
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,6 +42,65 @@ class AccountMove(models.Model):
|
||||
compute="_compute_sales_dates", readonly=True,
|
||||
help="This information appear on invoice qweb report "
|
||||
"(you may use it for your own report)")
|
||||
# There is a native "blocked" field (bool) on account.move.line
|
||||
# We want to have that field on invoices to improve usability
|
||||
# while keeping compatibility with the standard Odoo datamodel
|
||||
blocked = fields.Boolean(
|
||||
compute="_compute_blocked",
|
||||
inverse="_inverse_blocked",
|
||||
store=True,
|
||||
string="Dispute",
|
||||
tracking=True,
|
||||
)
|
||||
# Having amounts in invoice currency can be useful in tree view of invoices
|
||||
# We add those fields with optional="hide"
|
||||
amount_untaxed_invoice_currency_signed = fields.Monetary(
|
||||
compute="_compute_amount_invoice_currency_signed", store=True,
|
||||
string="Untaxed Amount Invoice Currency Signed")
|
||||
amount_tax_invoice_currency_signed = fields.Monetary(
|
||||
compute="_compute_amount_invoice_currency_signed", store=True,
|
||||
string="Tax Invoice Currency Signed")
|
||||
amount_total_invoice_currency_signed = fields.Monetary(
|
||||
compute="_compute_amount_invoice_currency_signed", store=True,
|
||||
string="Total Invoice Currency Signed")
|
||||
amount_residual_invoice_currency_signed = fields.Monetary(
|
||||
compute="_compute_amount_invoice_currency_signed", store=True,
|
||||
string="Amount Due Invoice Currency Signed")
|
||||
|
||||
@api.depends('amount_untaxed', 'amount_tax', 'amount_total', 'amount_residual', 'move_type')
|
||||
def _compute_amount_invoice_currency_signed(self):
|
||||
for move in self:
|
||||
amount_untaxed_invoice_currency_signed = move.amount_untaxed
|
||||
amount_tax_invoice_currency_signed = move.amount_tax
|
||||
amount_total_invoice_currency_signed = move.amount_total
|
||||
amount_residual_invoice_currency_signed = move.amount_residual
|
||||
if move.move_type in ('out_refund', 'in_refund'):
|
||||
amount_untaxed_invoice_currency_signed *= -1
|
||||
amount_tax_invoice_currency_signed *= -1
|
||||
amount_total_invoice_currency_signed *= -1
|
||||
amount_residual_invoice_currency_signed *= -1
|
||||
move.amount_untaxed_invoice_currency_signed = amount_untaxed_invoice_currency_signed
|
||||
move.amount_tax_invoice_currency_signed = amount_tax_invoice_currency_signed
|
||||
move.amount_total_invoice_currency_signed = amount_total_invoice_currency_signed
|
||||
move.amount_residual_invoice_currency_signed = amount_residual_invoice_currency_signed
|
||||
|
||||
@api.depends("line_ids", "line_ids.blocked")
|
||||
def _compute_blocked(self):
|
||||
for move in self:
|
||||
move.blocked = any(
|
||||
[
|
||||
l.blocked
|
||||
for l in move.line_ids
|
||||
if l.account_id.internal_type in ("payable", "receivable")
|
||||
]
|
||||
)
|
||||
|
||||
def _inverse_blocked(self):
|
||||
for move in self:
|
||||
for line in move.line_ids.filtered(
|
||||
lambda l: l.account_id.internal_type in ("payable", "receivable")
|
||||
):
|
||||
line.blocked = move.blocked
|
||||
|
||||
def _compute_has_discount(self):
|
||||
prec = self.env['decimal.precision'].precision_get('Discount')
|
||||
@@ -195,13 +256,11 @@ class AccountMove(models.Model):
|
||||
move.suitable_journal_ids = self.env['account.journal'].search(domain)
|
||||
|
||||
def button_draft(self):
|
||||
# Get report name before reset to draft because name can be different.
|
||||
report_names = self._get_invoice_attachment_name()
|
||||
super().button_draft()
|
||||
# Delete attached pdf invoice
|
||||
try:
|
||||
report_invoice = self.env['ir.actions.report']._get_report_from_name('account.report_invoice')
|
||||
except IndexError:
|
||||
report_invoice = False
|
||||
if report_invoice and report_invoice.attachment:
|
||||
if report_names:
|
||||
for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
|
||||
# The pb is that the filename is dynamic and related to move.state
|
||||
# in v12, the feature was native and they used that kind of code:
|
||||
@@ -211,7 +270,7 @@ class AccountMove(models.Model):
|
||||
# But do_in_draft() doesn't exists in v14
|
||||
# If you know how we could do that, please update the code below
|
||||
attachment = self.env['ir.attachment'].search([
|
||||
('name', '=', self._get_invoice_attachment_name()),
|
||||
('name', 'in', report_names[move.id]),
|
||||
('res_id', '=', move.id),
|
||||
('res_model', '=', self._name),
|
||||
('type', '=', 'binary'),
|
||||
@@ -220,8 +279,22 @@ class AccountMove(models.Model):
|
||||
attachment.unlink()
|
||||
|
||||
def _get_invoice_attachment_name(self):
|
||||
self.ensure_one()
|
||||
return '%s.pdf' % (self.name and self.name.replace('/', '_') or 'INV')
|
||||
report_names = defaultdict(list)
|
||||
try:
|
||||
report_invoice = self.env['ir.actions.report']._get_report_from_name('account.report_invoice')
|
||||
except IndexError:
|
||||
report_invoice = False
|
||||
if report_invoice and report_invoice.attachment:
|
||||
for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
|
||||
report_names[move.id].append(safe_eval(report_invoice.print_report_name, {'object': self, 'time': time}))
|
||||
try:
|
||||
report_invoice = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments')
|
||||
except IndexError:
|
||||
report_invoice = False
|
||||
if report_invoice and report_invoice.attachment:
|
||||
for move in self.filtered(lambda x: x.move_type in ('out_invoice', 'out_refund')):
|
||||
report_names[move.id].append(safe_eval(report_invoice.print_report_name, {'object': self, 'time': time}))
|
||||
return report_names
|
||||
|
||||
def _get_accounting_date(self, invoice_date, has_tax):
|
||||
# On vendor bills/refunds, we want date = invoice_date unless
|
||||
@@ -235,6 +308,40 @@ class AccountMove(models.Model):
|
||||
date = invoice_date
|
||||
return date
|
||||
|
||||
# I don't use account_invoice_supplier_ref_unique because it adds
|
||||
# a field supplier_invoice_number on account.move instead of using the native field
|
||||
# cf https://github.com/OCA/account-invoicing/issues/1484
|
||||
# So I take inspiration from the code of account_invoice_supplier_ref_unique
|
||||
# but I use the native "ref" field
|
||||
@api.constrains("ref", "partner_id")
|
||||
def _check_in_invoice_ref_unique_insensitive(self):
|
||||
for move in self:
|
||||
if move.ref and move.is_purchase_document(
|
||||
include_receipts=True
|
||||
):
|
||||
in_invoice_same_ref = self.search(
|
||||
[
|
||||
("commercial_partner_id", "=", move.commercial_partner_id.id),
|
||||
("move_type", "in", ("in_invoice", "in_refund")),
|
||||
("company_id", "=", move.company_id.id),
|
||||
("ref", "=ilike", move.ref),
|
||||
("id", "!=", move.id),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if in_invoice_same_ref:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"An invoice already exists in Odoo with the same "
|
||||
"bill reference '%s' for the same supplier '%s': %s."
|
||||
)
|
||||
% (
|
||||
in_invoice_same_ref.ref,
|
||||
in_invoice_same_ref.partner_id.display_name,
|
||||
in_invoice_same_ref.display_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
@@ -270,23 +377,18 @@ class AccountMoveLine(models.Model):
|
||||
def update_matching_number(self):
|
||||
records = self.search([("matching_number", "=", "P")])
|
||||
_logger.info(f"Update partial reconcile number for {len(records)} lines")
|
||||
records.compute_partial_matching_number()
|
||||
records._compute_matching_number()
|
||||
|
||||
def compute_partial_matching_number(self):
|
||||
def _compute_matching_number(self):
|
||||
# TODO maybe it will be better to have the same maching_number for
|
||||
# all partial so it will be easier to group by
|
||||
super()._compute_matching_number()
|
||||
for record in self:
|
||||
if record.matching_number == "P":
|
||||
matching_number = ", ".join([
|
||||
record.matching_number = ", ".join([
|
||||
"a%d" % pr.id
|
||||
for pr in record.matched_debit_ids + record.matched_credit_ids
|
||||
])
|
||||
# use sql to avoid triggering python checks
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
UPDATE account_move_line SET matching_number = %s WHERE id = %s
|
||||
""", (matching_number, record.id)
|
||||
)
|
||||
|
||||
def _get_computed_name(self):
|
||||
# This is useful when you want to have the product code in a dedicated
|
||||
|
||||
@@ -16,14 +16,6 @@
|
||||
<button name="button_reopen" position="attributes">
|
||||
<attribute name="confirm">Are you sure ? Don't do 'Reset to New' if you just want to modify the bank journal entry of an existing statement line.</attribute>
|
||||
</button>
|
||||
<button name="button_reopen" position="after">
|
||||
<button
|
||||
name="button_undo_reconciliation"
|
||||
type="object"
|
||||
confirm="Are you sure to unreconcile all the entries of the bank statement?"
|
||||
states="open"
|
||||
string="Unreconcile All"/>
|
||||
</button>
|
||||
<xpath expr="//field[@name='line_ids']/tree/button[@name='button_undo_reconciliation']" position="after">
|
||||
<field name="move_id" invisible="1"/>
|
||||
<button name="show_account_move" type="object"
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
<xpath expr="//field[@name='invoice_line_ids']/tree/field[@name='product_id']" position="after">
|
||||
<field name="product_barcode" optional="hide"/>
|
||||
</xpath>
|
||||
<field name="auto_post" position="before">
|
||||
<field name="blocked"/>
|
||||
</field>
|
||||
<div role="alert" position="after">
|
||||
<div id="warn_blocked" groups="account.group_account_invoice,account.group_account_readonly"
|
||||
class="alert alert-warning" role="alert" style="margin-bottom:0px;"
|
||||
attrs="{'invisible': ['|', ('move_type', 'not in', ('in_invoice', 'in_refund', 'out_invoice', 'out_refund')), ('blocked', '=', False)]}">
|
||||
This <field name="move_type"/> is marked as <b>disputed</b>.
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -57,6 +67,14 @@
|
||||
<field name="amount_residual_signed" position="attributes">
|
||||
<attribute name="optional">show</attribute>
|
||||
</field>
|
||||
<field name="amount_untaxed_signed" position="before">
|
||||
<!-- No sum="1" on the invoice currency fields, because it doesn't make sense
|
||||
to add amounts in different currencies -->
|
||||
<field name="amount_untaxed_invoice_currency_signed" string="Tax Excluded Inv. Cur." optional="hide"/>
|
||||
<field name="amount_tax_invoice_currency_signed" string="Tax Inv. Cur." optional="hide"/>
|
||||
<field name="amount_total_invoice_currency_signed" string="Total Inv. Cur." optional="hide"/>
|
||||
<field name="amount_residual_invoice_currency_signed" string="Amount Due Inv. Cur." optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -71,6 +89,8 @@
|
||||
<filter name="sent" string="Sent" domain="[('is_move_sent', '=', True), ('move_type', 'in', ('out_invoice', 'out_refund'))]"/>
|
||||
<separator/>
|
||||
<filter name="no_attachment" string="Missing Attachment" domain="[('has_attachment', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter name="dispute" string="Dispute" domain="[('blocked', '=', True)]"/>
|
||||
</filter>
|
||||
<filter name="salesperson" position="before">
|
||||
<filter name="commercial_partner_groupby" string="Commercial Partner" context="{'group_by': 'commercial_partner_id'}"/>
|
||||
@@ -81,6 +101,23 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_form" model="ir.ui.view">
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- The field 'blocked' is alone in it's block
|
||||
We don't want to display an empty block, so we put the attrs on the group
|
||||
The drawback of this is that, if someone added a field in that group,
|
||||
he won't see the field when internal_type is not payable/receivable -->
|
||||
<xpath expr="//field[@name='blocked']/.." position="attributes">
|
||||
<attribute name="attrs">{'invisible': [('account_internal_type', 'not in', ('payable', 'receivable'))]}</attribute>
|
||||
</xpath>
|
||||
<field name="company_id" position="after">
|
||||
<field name="account_internal_type" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree" model="ir.ui.view">
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_tree"/>
|
||||
@@ -126,6 +163,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_pivot" model="ir.ui.view">
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_pivot"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- By default, date is split by month... but if you've been using Odoo for several years,
|
||||
the pivot table becomes very big by default: so we split by year -->
|
||||
<field name="date" position="attributes">
|
||||
<attribute name="interval">year</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="account.action_move_journal_line" model="ir.actions.act_window">
|
||||
<field name="context">{'default_move_type': 'entry', 'view_no_maturity': True}</field>
|
||||
<!-- Remove 'search_default_misc_filter': 1 -->
|
||||
|
||||
21
account_usability/views/res_partner.xml
Normal file
21
account_usability/views/res_partner.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2024 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_property_form" model="ir.ui.view">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="account.view_partner_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='bank_ids']/tree/field[@name='acc_holder_name']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
<attribute name="optional">hide</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -15,6 +15,7 @@ class ResPartnerPhone(models.Model):
|
||||
_name = 'res.partner.phone'
|
||||
_order = 'partner_id, type'
|
||||
_phone_name_sequence = 8
|
||||
_phone_name_fields = ["phone"]
|
||||
_inherit = ['phone.validation.mixin']
|
||||
_description = 'Multiple emails and phones for partners'
|
||||
|
||||
@@ -73,7 +74,7 @@ class ResPartnerPhone(models.Model):
|
||||
if self._context.get('callerid'):
|
||||
name = pphone.partner_id.display_name
|
||||
else:
|
||||
name = u'%s (%s)' % (pphone.phone, pphone.partner_id.name)
|
||||
name = '%s (%s)' % (pphone.phone, pphone.partner_id.name)
|
||||
else:
|
||||
name = pphone.phone
|
||||
res.append((pphone.id, name))
|
||||
@@ -104,6 +105,7 @@ class ResPartnerPhone(models.Model):
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
_phone_name_fields = []
|
||||
|
||||
# in v10, we are supposed to have in DB E.164 format
|
||||
# with the current implementation, we have:
|
||||
@@ -147,6 +149,8 @@ class ResPartner(models.Model):
|
||||
if vals.get(partner_field):
|
||||
vals['phone_ids'].append(
|
||||
(0, 0, {'type': type, partner_phone_field: vals[partner_field]}))
|
||||
if partner_field in vals:
|
||||
vals.pop(partner_field)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
@@ -155,7 +159,6 @@ class ResPartner(models.Model):
|
||||
self._update_create_vals(vals, '1_email_primary', 'email', 'email')
|
||||
self._update_create_vals(vals, '3_phone_primary', 'phone', 'phone')
|
||||
self._update_create_vals(vals, '5_mobile_primary', 'mobile', 'phone')
|
||||
# self._update_create_vals(vals, '7_fax_primary', 'fax', 'phone')
|
||||
return super().create(vals)
|
||||
|
||||
def _update_write_vals(
|
||||
@@ -178,16 +181,16 @@ class ResPartner(models.Model):
|
||||
else:
|
||||
if pphone:
|
||||
vals['phone_ids'].append((2, pphone.id))
|
||||
vals.pop(partner_field)
|
||||
|
||||
def write(self, vals):
|
||||
if 'phone_ids' not in vals:
|
||||
for rec in self:
|
||||
vals['phone_ids'] = []
|
||||
rec._update_write_vals(vals, '1_email_primary', 'email', 'email')
|
||||
rec._update_write_vals(vals, '3_phone_primary', 'phone', 'phone')
|
||||
rec._update_write_vals(vals, '5_mobile_primary', 'mobile', 'phone')
|
||||
rec._update_write_vals(vals, '7_fax_primary', 'fax', 'phone')
|
||||
super(ResPartner, rec).write(vals)
|
||||
cvals = dict(vals, phone_ids=[])
|
||||
rec._update_write_vals(cvals, '1_email_primary', 'email', 'email')
|
||||
rec._update_write_vals(cvals, '3_phone_primary', 'phone', 'phone')
|
||||
rec._update_write_vals(cvals, '5_mobile_primary', 'mobile', 'phone')
|
||||
super(ResPartner, rec).write(cvals)
|
||||
return True
|
||||
else:
|
||||
return super().write(vals)
|
||||
|
||||
@@ -161,6 +161,12 @@
|
||||
<field name="name" position="attributes">
|
||||
<attribute name="filter_domain">['|', '|', ('display_name', 'ilike', self), ('ref', '=ilike', self + '%'), ('phone_ids.email', 'ilike', self)]</attribute>
|
||||
</field>
|
||||
<field name="email" position="attributes">
|
||||
<attribute name="filter_domain">[('phone_ids.email', 'ilike', self)]</attribute>
|
||||
</field>
|
||||
<field name="phone" position="attributes">
|
||||
<attribute name="filter_domain">[('phone_ids.phone', 'ilike', self)]</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
'summary': 'Better usability in base module',
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['base'],
|
||||
'depends': ['web'],
|
||||
'data': [
|
||||
'security/group.xml',
|
||||
'security/ir.model.access.csv',
|
||||
@@ -21,6 +21,7 @@
|
||||
'views/ir_module.xml',
|
||||
'views/ir_sequence.xml',
|
||||
'views/ir_property.xml',
|
||||
'views/assets.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-07-01 10:02+0000\n"
|
||||
"PO-Revision-Date: 2021-07-01 10:02+0000\n"
|
||||
"POT-Creation-Date: 2024-03-26 21:27+0000\n"
|
||||
"PO-Revision-Date: 2024-03-26 21:27+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -15,6 +15,18 @@ 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"
|
||||
@@ -25,11 +37,22 @@ 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"
|
||||
@@ -48,6 +71,7 @@ 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
|
||||
@@ -65,12 +89,24 @@ 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
|
||||
@@ -87,6 +123,7 @@ 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
|
||||
@@ -139,6 +176,11 @@ 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
|
||||
@@ -146,8 +188,14 @@ msgid "Reference"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
|
||||
msgid "Search Countries"
|
||||
#: 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:"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
@@ -169,6 +217,14 @@ 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,15 +6,27 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \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"
|
||||
"POT-Creation-Date: 2024-03-26 21:27+0000\n"
|
||||
"PO-Revision-Date: 2024-03-26 21:27+0000\n"
|
||||
"Last-Translator: \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"
|
||||
@@ -25,11 +37,22 @@ 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"
|
||||
@@ -42,11 +65,13 @@ 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
|
||||
@@ -64,12 +89,24 @@ 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
|
||||
@@ -78,7 +115,7 @@ msgstr "Grouper par"
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__id
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_users__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: base_usability
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.view_module_filter
|
||||
@@ -86,6 +123,7 @@ 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
|
||||
@@ -136,7 +174,12 @@ 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 du partenaire"
|
||||
msgstr "Étiquettes contact"
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model.fields,field_description:base_usability.field_ir_actions_report__print_report_name
|
||||
msgid "Printed Report Name"
|
||||
msgstr "Nom du rapport imprimé"
|
||||
|
||||
#. module: base_usability
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_partner__ref
|
||||
@@ -145,11 +188,18 @@ msgid "Reference"
|
||||
msgstr "Référence"
|
||||
|
||||
#. module: base_usability
|
||||
#: model_terms:ir.ui.view,arch_db:base_usability.res_country_search
|
||||
msgid "Search Countries"
|
||||
#: 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:"
|
||||
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:"
|
||||
@@ -158,7 +208,7 @@ msgstr "N° fournisseur :"
|
||||
#. module: base_usability
|
||||
#: model:ir.model.fields,field_description:base_usability.field_res_partner_category__name
|
||||
msgid "Tag Name"
|
||||
msgstr "Nom de l'étiquette"
|
||||
msgstr "Libellé de l'étiquette"
|
||||
|
||||
#. module: base_usability
|
||||
#: code:addons/base_usability/models/res_company.py:0
|
||||
@@ -167,6 +217,14 @@ msgstr "Nom 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,6 +3,7 @@
|
||||
# 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):
|
||||
@@ -39,32 +40,78 @@ 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:')},
|
||||
'label': _('VAT:'),
|
||||
},
|
||||
'ape': {
|
||||
'value': hasattr(self, 'ape') and self.ape or False,
|
||||
'label': _('APE:'),
|
||||
},
|
||||
'siret': {
|
||||
'value': hasattr(self, 'siret') and self.siret or False,
|
||||
'label': _('SIRET:'),
|
||||
},
|
||||
'siren': {
|
||||
'value': hasattr(self, 'siren') and self.siren or False,
|
||||
'label': _('SIREN:'),
|
||||
},
|
||||
'eori': {
|
||||
'value': self._get_eori(),
|
||||
'label': _('EORI:'),
|
||||
},
|
||||
'capital': {
|
||||
# 'capital_amount' added by base_company_extension
|
||||
'value': hasattr(self, 'capital_amount') and self.capital_amount and format_amount(self.env, self.capital_amount, self.currency_id) or False,
|
||||
'label': _('Capital:'),
|
||||
}
|
||||
}
|
||||
# 'legal_type' added by base_company_extension
|
||||
if hasattr(self, 'legal_type') and self.legal_type:
|
||||
options['capital']['label'] = _('%s with a capital of') % self.legal_type
|
||||
return options
|
||||
|
||||
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=[['phone', 'website'], ['vat']],
|
||||
icon=True, line_separator=' - '):
|
||||
self, line_details=None, 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,6 +125,20 @@ 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']:
|
||||
|
||||
10
base_usability/static/src/scss/form_view.scss
Normal file
10
base_usability/static/src/scss/form_view.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.o_form_view {
|
||||
.o_address_format {
|
||||
.o_address_state {
|
||||
margin-right: 0;
|
||||
}
|
||||
.o_address_zip {
|
||||
margin-right: 2%;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
base_usability/views/assets.xml
Normal file
15
base_usability/views/assets.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template
|
||||
id="assets_backend"
|
||||
name="web_sheet_full_width"
|
||||
inherit_id="web.assets_backend"
|
||||
>
|
||||
<xpath expr="." position="inside">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/base_usability/static/src/scss/form_view.scss"
|
||||
/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -20,6 +20,21 @@
|
||||
<div attrs="{'invisible': [('same_vat_partner_id', '=', False)]}" position="attributes">
|
||||
<attribute name="class">alert alert-warning</attribute>
|
||||
</div>
|
||||
<!-- Native order: city / state_id / zip. New order: zip / city / state_id -->
|
||||
<xpath expr="//div[hasclass('o_address_format')]/field[@name='city']" position="before">
|
||||
<field name="zip" position="move"/>
|
||||
</xpath>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
<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>
|
||||
|
||||
28
base_usability/web-advanced_search_startswith.diff
Normal file
28
base_usability/web-advanced_search_startswith.diff
Normal file
@@ -0,0 +1,28 @@
|
||||
diff --git a/addons/web/static/src/js/control_panel/custom_filter_item.js b/addons/web/static/src/js/control_panel/custom_filter_item.js
|
||||
index 6682b660b78..b2ff23344d9 100644
|
||||
--- a/addons/web/static/src/js/control_panel/custom_filter_item.js
|
||||
+++ b/addons/web/static/src/js/control_panel/custom_filter_item.js
|
||||
@@ -180,6 +180,10 @@ odoo.define('web.CustomFilterItem', function (require) {
|
||||
[field.name, '>=', domainValue[0]],
|
||||
[field.name, '<=', domainValue[1]]
|
||||
);
|
||||
+ } else if (operator.symbol === 'startswith') {
|
||||
+ domainArray.push([field.name, '=ilike', domainValue[0] + '%']);
|
||||
+ } else if (operator.symbol === 'endswith') {
|
||||
+ domainArray.push([field.name, '=ilike', '%' + domainValue[0]]);
|
||||
} else {
|
||||
domainArray.push([field.name, operator.symbol, domainValue[0]]);
|
||||
}
|
||||
diff --git a/addons/web/static/src/js/control_panel/search_utils.js b/addons/web/static/src/js/control_panel/search_utils.js
|
||||
index 8fce5b23ef6..d240d2e1fb2 100644
|
||||
--- a/addons/web/static/src/js/control_panel/search_utils.js
|
||||
+++ b/addons/web/static/src/js/control_panel/search_utils.js
|
||||
@@ -18,6 +18,8 @@ odoo.define('web.searchUtils', function (require) {
|
||||
char: [
|
||||
{ symbol: "ilike", description: _lt("contains") },
|
||||
{ symbol: "not ilike", description: _lt("doesn't contain") },
|
||||
+ { symbol: "startswith", description: _lt("starts with") },
|
||||
+ { symbol: "endswith", description: _lt("ends with") },
|
||||
{ symbol: "=", description: _lt("is equal to") },
|
||||
{ symbol: "!=", description: _lt("is not equal to") },
|
||||
{ symbol: "!=", description: _lt("is set"), value: false },
|
||||
@@ -22,6 +22,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
|
||||
'depends': ['delivery'],
|
||||
'data': [
|
||||
'views/stock_picking.xml',
|
||||
'views/product_packaging.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
from . import product_packaging
|
||||
from . import stock_picking
|
||||
from . import stock_move
|
||||
from . import stock_quant_package
|
||||
|
||||
31
delivery_usability/models/product_packaging.py
Normal file
31
delivery_usability/models/product_packaging.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright 2018-2021 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 fields, models
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
|
||||
# product.packaging is defined in the 'product' module and enhanced in the 'delivery' module
|
||||
# I used to make the improvements on the datamodel of product.packaging in the OCA module
|
||||
# 'stock_packaging_usability_pp' from OCA/stock-logistics-tracking,
|
||||
# but I eventually figured out that the feature provided by 'stock_packaging_usability_pp'
|
||||
# was native in the 'delivery' module via the wizard choose.delivery.package.
|
||||
# So I stopped using 'stock_packaging_usability_pp' and I moved the datamodel changes
|
||||
# here in the module 'delivery_usability'
|
||||
name = fields.Char(translate=True)
|
||||
weight = fields.Float(digits="Stock Weight", string="Empty Package Weight")
|
||||
active = fields.Boolean(default=True)
|
||||
# packaging_type is important, in particular for pallets for which
|
||||
# we need a special implementation to enter the height
|
||||
packaging_type = fields.Selection(
|
||||
[
|
||||
("unit", "Unit"),
|
||||
("pack", "Pack"),
|
||||
("box", "Box"),
|
||||
("pallet", "Pallet"),
|
||||
],
|
||||
string="Type",
|
||||
)
|
||||
23
delivery_usability/models/stock_move.py
Normal file
23
delivery_usability/models/stock_move.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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 api, models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
# Fixing bug https://github.com/odoo/odoo/issues/34702
|
||||
@api.depends("product_id", "product_uom_qty", "product_uom")
|
||||
def _cal_move_weight(self):
|
||||
weight_uom_categ = self.env.ref("uom.product_uom_categ_kgm")
|
||||
kg_uom = self.env.ref("uom.product_uom_kgm")
|
||||
for move in self:
|
||||
if move.product_id.uom_id.category_id == weight_uom_categ:
|
||||
move.weight = move.product_id.uom_id._compute_quantity(
|
||||
move.product_qty, kg_uom
|
||||
)
|
||||
else:
|
||||
move.weight = move.product_qty * move.product_id.weight
|
||||
68
delivery_usability/models/stock_quant_package.py
Normal file
68
delivery_usability/models/stock_quant_package.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Copyright 2019-2024 Akretion France (https://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_is_zero
|
||||
|
||||
|
||||
class StockQuantPackage(models.Model):
|
||||
_inherit = "stock.quant.package"
|
||||
|
||||
# These 2 fields are defined in the 'delivery' module but they forgot
|
||||
# the decimal precision
|
||||
shipping_weight = fields.Float(digits="Stock Weight")
|
||||
weight = fields.Float(digits="Stock Weight")
|
||||
|
||||
# Fixing bug https://github.com/odoo/odoo/issues/34702
|
||||
# and take into account the weight of the packaging
|
||||
# WARNING: this method _compute_weight() is also inherited by the OCA module
|
||||
# base_delivery_carrier_label so if you use that module, you should copy
|
||||
# that piece of code in a custom module that depend on delivery_usability
|
||||
# and base_delivery_carrier_label
|
||||
def _compute_weight(self):
|
||||
smlo = self.env["stock.move.line"]
|
||||
weight_uom_categ = self.env.ref("uom.product_uom_categ_kgm")
|
||||
kg_uom = self.env.ref("uom.product_uom_kgm")
|
||||
weight_prec = self.env['decimal.precision'].precision_get('Stock Weight')
|
||||
for package in self:
|
||||
# if the weight of the package has been measured,
|
||||
# it is written in shipping_weight
|
||||
if not float_is_zero(package.shipping_weight, precision_digits=weight_prec):
|
||||
weight = package.shipping_weight
|
||||
# otherwise, we compute the theorical weight from the weight of the products
|
||||
# and the weight of the packaging
|
||||
# Since Odoo v11, consu products don't create quants, so I can't loop
|
||||
# on pack.quant_ids to get all the items inside a package: I have to
|
||||
# get the picking, then loop on the stock.move.line of that picking
|
||||
# linked to that package
|
||||
else:
|
||||
weight = 0.0
|
||||
# the package can be seen in a return
|
||||
# So I get the picking of it's first appearance
|
||||
domain = [
|
||||
("result_package_id", "=", package.id),
|
||||
("product_id", "!=", False),
|
||||
]
|
||||
first_move_line = smlo.search(
|
||||
domain + [('picking_id', '!=', False)], limit=1, order='id')
|
||||
if first_move_line:
|
||||
picking_id = first_move_line.picking_id.id
|
||||
current_picking_move_line_ids = smlo.search(
|
||||
domain + [("picking_id", "=", picking_id)])
|
||||
for ml in current_picking_move_line_ids:
|
||||
if ml.product_uom_id.category_id == weight_uom_categ:
|
||||
weight += ml.product_uom_id._compute_quantity(
|
||||
ml.qty_done, kg_uom
|
||||
)
|
||||
else:
|
||||
weight += (
|
||||
ml.product_uom_id._compute_quantity(
|
||||
ml.qty_done, ml.product_id.uom_id
|
||||
)
|
||||
* ml.product_id.weight
|
||||
)
|
||||
if package.packaging_id:
|
||||
weight += package.packaging_id.weight
|
||||
package.weight = weight
|
||||
79
delivery_usability/views/product_packaging.xml
Normal file
79
delivery_usability/views/product_packaging.xml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2019-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>
|
||||
|
||||
|
||||
<!-- I don't know why the form view of product.packaging in the delivery
|
||||
module has "<field name="inherit_id" eval="False"/>"
|
||||
instead of a standard inherit of product.product_packaging_form_view -->
|
||||
<record id="product_packaging_delivery_form" model="ir.ui.view">
|
||||
<field name="name">stock_packaging_usability_pp.product.packaging.form</field>
|
||||
<field name="model">product.packaging</field>
|
||||
<field name="inherit_id" ref="delivery.product_packaging_delivery_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="package_carrier_type" position="after">
|
||||
<field name="packaging_type" />
|
||||
<field name="active" invisible="1" />
|
||||
</field>
|
||||
<label for="max_weight" position="before">
|
||||
<label for="weight" />
|
||||
<div class="o_row" name="weight">
|
||||
<field name="weight" />
|
||||
<span><field name="weight_uom_name" /></span>
|
||||
</div>
|
||||
</label>
|
||||
<label for="name" position="before">
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Archived"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': [('active', '=', True)]}"
|
||||
/>
|
||||
</label>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_packaging_delivery_tree" model="ir.ui.view">
|
||||
<field name="name">stock_packaging_usability_pp.product.packaging.tree</field>
|
||||
<field name="model">product.packaging</field>
|
||||
<field name="inherit_id" ref="delivery.product_packaging_delivery_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="packaging_type" optional="show" />
|
||||
</field>
|
||||
<field name="max_weight" position="before">
|
||||
<field name="weight" optional="show" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- There is no native serch view for product.packaging -->
|
||||
<record id="product_packaging_search" model="ir.ui.view">
|
||||
<field name="name">product.packaging.search</field>
|
||||
<field name="model">product.packaging</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" />
|
||||
<separator />
|
||||
<filter
|
||||
string="Archived"
|
||||
name="inactive"
|
||||
domain="[('active', '=', False)]"
|
||||
/>
|
||||
<group name="groupby">
|
||||
<filter
|
||||
name="packaging_type_groupby"
|
||||
string="Packaging Type"
|
||||
context="{'group_by': 'packaging_type'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1,4 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import intrastat_product_type
|
||||
from .post_install import set_intrastat_type_on_products
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{
|
||||
'name': 'Intrastat Product Type',
|
||||
'version': '12.0.1.0.0',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Accounting',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Adds a special field Intrastat Type on Products',
|
||||
@@ -19,9 +19,8 @@ This module adds a field *Intrastat Type* on the Product Form with 2 possible op
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['intrastat_product', 'l10n_fr_intrastat_service'],
|
||||
'data': ['product_view.xml'],
|
||||
'post_init_hook': 'set_intrastat_type_on_products',
|
||||
'installable': False,
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016-2019 Akretion (http://www.akretion.com)
|
||||
# Copyright 2016-2023 Akretion (http://www.akretion.com)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
|
||||
@@ -14,50 +13,36 @@ class ProductTemplate(models.Model):
|
||||
intrastat_type = fields.Selection([
|
||||
('product', 'Product'),
|
||||
('service', 'Service'),
|
||||
], string='Intrastat Type', default='product', required=True,
|
||||
],
|
||||
compute='_compute_intrastat_type', readonly=False, store=True,
|
||||
required=True, string='Intrastat Type',
|
||||
help="Type of product used for the intrastat declarations. "
|
||||
"For example, you can configure a product with "
|
||||
"'Product Type' = 'Consumable' and 'Intrastat Type' = 'Service'.")
|
||||
|
||||
@api.multi
|
||||
@api.constrains('type', 'intrastat_type')
|
||||
def check_intrastat_type(self):
|
||||
for pt in self:
|
||||
if pt.intrastat_type == 'product' and pt.type == 'service':
|
||||
raise ValidationError(_(
|
||||
"On the product %s, you cannot set Product Type to "
|
||||
"'Service' and Intrastat Type to 'Product'.") % pt.name)
|
||||
"On the product '%s', you cannot set Product Type to "
|
||||
"'Service' and Intrastat Type to 'Product'.")
|
||||
% pt.display_name)
|
||||
if pt.intrastat_type == 'service' and pt.type == 'product':
|
||||
raise ValidationError(_(
|
||||
"On the product %s, you cannot set Intrastat Type to "
|
||||
"On the product '%s', you cannot set Intrastat Type to "
|
||||
"'Service' and Product Type to 'Stockable product' "
|
||||
"(but you can set Product Type to 'Consumable' or "
|
||||
"'Service').") % pt.name)
|
||||
"'Service').") % pt.display_name)
|
||||
|
||||
@api.onchange('type')
|
||||
def intrastat_type_onchange(self):
|
||||
if self.type in ('product', 'consu'):
|
||||
self.intrastat_type = 'product'
|
||||
elif self.type == 'service':
|
||||
self.intrastat_type = 'service'
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('type'):
|
||||
if not vals.get('intrastat_type'):
|
||||
if vals['type'] in ('product', 'consu'):
|
||||
vals['intrastat_type'] = 'product'
|
||||
elif vals['type'] == 'service':
|
||||
vals['intrastat_type'] = 'service'
|
||||
elif (
|
||||
vals.get('intrastat_type') == 'product' and
|
||||
vals['type'] == 'service'):
|
||||
# usefull because intrastat_type = 'product' by default and
|
||||
# wizards in other modules that don't depend on this module
|
||||
# (e.g. sale_rental) may create a product with only
|
||||
# {'type': 'service'} and no 'intrastat_type'
|
||||
vals['intrastat_type'] = 'service'
|
||||
return super(ProductTemplate, self).create(vals)
|
||||
@api.depends('type')
|
||||
def _compute_intrastat_type(self):
|
||||
for pt in self:
|
||||
if pt.type in ('product', 'consu'):
|
||||
intrastat_type = 'product'
|
||||
else:
|
||||
intrastat_type = 'service'
|
||||
pt.intrastat_type = intrastat_type
|
||||
|
||||
|
||||
class L10nFrIntrastatServiceDeclaration(models.Model):
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016-2019 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
def set_intrastat_type_on_products(cr, registry):
|
||||
cr.execute(
|
||||
"UPDATE product_template SET intrastat_type='service' "
|
||||
"WHERE type='service'")
|
||||
return
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2016-2019 Akretion (http://www.akretion.com/)
|
||||
Copyright 2016-2023 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).
|
||||
-->
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com)
|
||||
# Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'MRP Average Cost',
|
||||
'version': '12.0.1.0.0', # WARNING: we'll probably not port this module to v14, because part of its feature is now provided by the module mrp_account
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Manufactuing',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Update standard_price upon validation of a manufacturing order',
|
||||
@@ -12,16 +12,16 @@
|
||||
MRP Average Cost
|
||||
================
|
||||
|
||||
By default, the official stock module updates the standard_price of a product that has costing_method = 'average' when validating an incoming picking. But the official 'mrp' module doesn't do that when you validate a manufactuging order.
|
||||
I initially developped this module for Odoo 12.0, when the module mrp_account didn't exist, so Odoo didn't support the update of the standard cost of a manufactured product.
|
||||
|
||||
This module adds this feature : when you validate a manufacturing order of a product that has costing method = 'average', the standard_price of the product will be updated by taking into account the standard_price of each raw material and also a number of work hours defined on the BOM.
|
||||
In the mrp_account module, you must use workcenters to take the labor costs into account. This module aims at encoding theorical labor costs on the BOM and using it to compute the cost of the finished product.
|
||||
|
||||
Together with this module, I recommend the use of my module product_usability, available in the same branch, which contains a backport of the model product.price.history from v8 to v7.
|
||||
With this module, when you validate a manufacturing order of a product that has costing method = 'average', the standard_price of the product will be updated by taking into account the standard_price of each raw material and also a number of work hours defined on the BOM plus the extra cost defined of the BOM.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'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': False,
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# Copyright (C) 2016-2019 Akretion (http://www.akretion.com)
|
||||
# Copyright (C) 2016-2024 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
import odoo.addons.decimal_precision as dp
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
from odoo.tools import float_compare
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -17,23 +15,19 @@ class MrpBomLabourLine(models.Model):
|
||||
|
||||
bom_id = fields.Many2one(
|
||||
comodel_name='mrp.bom',
|
||||
string='Labour Lines',
|
||||
string='Bill of Material',
|
||||
ondelete='cascade')
|
||||
|
||||
labour_time = fields.Float(
|
||||
string='Labour Time',
|
||||
required=True,
|
||||
digits=dp.get_precision('Labour Hours'),
|
||||
digits='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(
|
||||
string='Note')
|
||||
note = fields.Text()
|
||||
|
||||
_sql_constraints = [(
|
||||
'labour_time_positive',
|
||||
@@ -44,6 +38,26 @@ 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')
|
||||
@@ -70,107 +84,77 @@ 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 bom')
|
||||
logger.info('Start to auto-update cost price from phantom boms')
|
||||
boms = self.search([('type', '=', 'phantom')])
|
||||
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
|
||||
boms.manual_update_product_standard_price()
|
||||
logger.info('End of the auto-update cost price from phantom boms')
|
||||
|
||||
def manual_update_product_standard_price(self):
|
||||
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(
|
||||
prec = self.env['decimal.precision'].precision_get(
|
||||
'Product Price')
|
||||
for bom in self:
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
class MrpBomLine(models.Model):
|
||||
_inherit = 'mrp.bom.line'
|
||||
|
||||
standard_price = fields.Float(
|
||||
related='product_id.standard_price',
|
||||
readonly=True,
|
||||
string='Standard Price')
|
||||
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
|
||||
|
||||
|
||||
class LabourCostProfile(models.Model):
|
||||
_name = 'labour.cost.profile'
|
||||
_inherit = ['mail.thread']
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_description = 'Labour Cost Profile'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
track_visibility='onchange')
|
||||
|
||||
tracking=True)
|
||||
hour_cost = fields.Float(
|
||||
string='Cost per Hour',
|
||||
required=True,
|
||||
digits=dp.get_precision('Product Price'),
|
||||
track_visibility='onchange',
|
||||
digits='Product Price',
|
||||
tracking=True,
|
||||
help="Labour cost per hour per person in company currency")
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company',
|
||||
string='Company',
|
||||
required=True,
|
||||
default=lambda self: self.env['res.company']._company_default_get())
|
||||
comodel_name='res.company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
|
||||
company_currency_id = fields.Many2one(
|
||||
related='company_id.currency_id',
|
||||
readonly=True,
|
||||
store=True,
|
||||
string='Company Currency')
|
||||
related='company_id.currency_id', 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, u'%s (%s %s)' % (
|
||||
res.append((record.id, '%s (%s %s)' % (
|
||||
record.name, record.hour_cost,
|
||||
record.company_currency_id.symbol)))
|
||||
return res
|
||||
@@ -179,93 +163,25 @@ 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', readonly=True,
|
||||
string='Company Currency')
|
||||
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.")
|
||||
|
||||
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
|
||||
# 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 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()
|
||||
@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
|
||||
|
||||
@@ -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', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Akretion (http://www.akretion.com/)
|
||||
Copyright (C) 2016-2024 Akretion (http://www.akretion.com/)
|
||||
@author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
@@ -13,23 +13,25 @@
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<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>
|
||||
<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>
|
||||
<notebook position="inside">
|
||||
<page string="Labour" name="labour_lines">
|
||||
<group name="labour_lines_grp">
|
||||
@@ -117,10 +119,10 @@
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view"/>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="availability" position="after">
|
||||
<field name="unit_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}" attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
<xpath expr="//page[@name='miscellaneous']//field[@name='origin']/.." position="inside">
|
||||
<field name="extra_cost" widget="monetary" options="{'currency_field': 'company_currency_id'}"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
</field>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{
|
||||
'name': 'MRP No Product Template Menu',
|
||||
'version': '12.0.1.0.0',
|
||||
'version': '14.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': 'http://www.akretion.com',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['mrp', 'sale_purchase_no_product_template_menu'],
|
||||
'auto_install': True,
|
||||
'data': ['mrp_view.xml'],
|
||||
'installable': False,
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2016-2019 Akretion France (http://www.akretion.com/)
|
||||
Copyright 2016-2024 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="product_product_action_mrp" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="context">{'search_default_consumable': 1, 'default_type': 'product'}</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp.menu_mrp_product_form" model="ir.ui.menu">
|
||||
<field name="action" ref="product_product_action_mrp"/>
|
||||
<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>
|
||||
|
||||
0
mrp_subcontracting_usability/__init__.py
Normal file
0
mrp_subcontracting_usability/__init__.py
Normal file
18
mrp_subcontracting_usability/__manifest__.py
Normal file
18
mrp_subcontracting_usability/__manifest__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copyright 2023 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 Subcontracting Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Manufacturing',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Usability improvements on mrp_subcontracting',
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': ['mrp_subcontracting'],
|
||||
'data': [
|
||||
'views/mrp_bom.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
22
mrp_subcontracting_usability/views/mrp_bom.xml
Normal file
22
mrp_subcontracting_usability/views/mrp_bom.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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_mrp_bom_filter" model="ir.ui.view">
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="inherit_id" ref="mrp.view_mrp_bom_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="phantom" position="after">
|
||||
<filter name="subcontract" domain="[('type', '=', 'subcontract')]" string="Subcontracting"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -14,6 +14,7 @@
|
||||
'data': [
|
||||
'views/mrp_production.xml',
|
||||
'views/product_template.xml',
|
||||
'views/stock_move_line.xml',
|
||||
# 'report/mrp_report.xml' # TODO
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -3,12 +3,23 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import api, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_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,8 +16,9 @@
|
||||
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_src_id']" position="replace"/>
|
||||
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='location_dest_id']" position="replace"/>
|
||||
<field name="bom_id" position="after">
|
||||
<field name="location_src_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<!-- no need to set readonly via attrs, the readonly status is in the field definition -->
|
||||
<field name="location_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>
|
||||
<xpath expr="//page[@name='miscellaneous']/group/group/field[@name='date_deadline']" position="after">
|
||||
<field name="date_start"/>
|
||||
@@ -40,4 +41,20 @@
|
||||
</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>
|
||||
|
||||
21
mrp_usability/views/stock_move_line.xml
Normal file
21
mrp_usability/views/stock_move_line.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2024 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_move_line_form" model="ir.ui.view">
|
||||
<field name="name">mrp_usability.stock.move.line.form</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="inherit_id" ref="stock.view_move_line_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="reference" position="before">
|
||||
<field name="production_id" attrs="{'invisible': [('production_id', '=', False)]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -31,7 +31,11 @@ Akretion:
|
||||
"views/report_pos_order.xml",
|
||||
"views/pos_category.xml",
|
||||
"views/pos_session.xml",
|
||||
"views/pos_order.xml",
|
||||
"views/pos_config.xml",
|
||||
"views/product.xml",
|
||||
"views/pos_payment_method.xml",
|
||||
"views/stock_warehouse.xml",
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from . import product
|
||||
from . import pos_category
|
||||
from . import pos_config
|
||||
from . import pos_payment_method
|
||||
|
||||
12
pos_usability/models/pos_config.py
Normal file
12
pos_usability/models/pos_config.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copyright 2023 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
|
||||
|
||||
|
||||
class PosConfig(models.Model):
|
||||
_inherit = 'pos.config'
|
||||
_order = 'sequence, id'
|
||||
|
||||
sequence = fields.Integer(default=10)
|
||||
@@ -8,5 +8,7 @@ from odoo import fields, models
|
||||
class PosPaymentMethod(models.Model):
|
||||
_inherit = 'pos.payment.method'
|
||||
_check_company_auto = True
|
||||
_order = 'sequence, id'
|
||||
|
||||
cash_journal_id = fields.Many2one(check_company=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
|
||||
35
pos_usability/pos-payment_terminal-auto_validate.diff
Normal file
35
pos_usability/pos-payment_terminal-auto_validate.diff
Normal file
@@ -0,0 +1,35 @@
|
||||
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
|
||||
index 55aa635aa10..d91ea933a71 100644
|
||||
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
|
||||
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
|
||||
@@ -8,6 +8,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { onChangeOrder } = require('point_of_sale.custom_hooks');
|
||||
+ const utils = require('web.utils');
|
||||
|
||||
class PaymentScreen extends PosComponent {
|
||||
constructor() {
|
||||
@@ -20,6 +21,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
|
||||
useListener('send-payment-cancel', this._sendPaymentCancel);
|
||||
useListener('send-payment-reverse', this._sendPaymentReverse);
|
||||
useListener('send-force-done', this._sendForceDone);
|
||||
+ useListener('validate-order', () => this.validateOrder(false));
|
||||
this.lockedValidateOrder = useAsyncLockedMethod(this.validateOrder);
|
||||
NumberBuffer.use(this._getNumberBufferConfig);
|
||||
onChangeOrder(this._onPrevOrder, this._onNewOrder);
|
||||
@@ -333,6 +335,14 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
|
||||
if (isPaymentSuccessful) {
|
||||
line.set_payment_status('done');
|
||||
line.can_be_reversed = payment_terminal.supports_reversals;
|
||||
+ // Automatically validate the order if, after an electronic payment,
|
||||
+ // the current order is fully paid (BACKPORT FROM v16)
|
||||
+ if (
|
||||
+ this.currentOrder.is_paid() &&
|
||||
+ utils.float_is_zero(this.currentOrder.get_due(), this.env.pos.currency.decimals)
|
||||
+ ) {
|
||||
+ this.trigger('validate-order');
|
||||
+ }
|
||||
} else {
|
||||
line.set_payment_status('retry');
|
||||
}
|
||||
22
pos_usability/pos_payment_method-sort_by_sequence.diff
Normal file
22
pos_usability/pos_payment_method-sort_by_sequence.diff
Normal file
@@ -0,0 +1,22 @@
|
||||
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
|
||||
index 86a7b44bcfb..06b46ba5645 100644
|
||||
--- a/addons/point_of_sale/static/src/js/models.js
|
||||
+++ b/addons/point_of_sale/static/src/js/models.js
|
||||
@@ -514,16 +514,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
fields: ['name', 'is_cash_count', 'use_payment_terminal'],
|
||||
domain: function(self){return ['|',['active', '=', false], ['active', '=', true]]; },
|
||||
loaded: function(self, payment_methods) {
|
||||
- self.payment_methods = payment_methods.sort(function(a,b){
|
||||
- // prefer cash payment_method to be first in the list
|
||||
- if (a.is_cash_count && !b.is_cash_count) {
|
||||
- return -1;
|
||||
- } else if (!a.is_cash_count && b.is_cash_count) {
|
||||
- return 1;
|
||||
- } else {
|
||||
- return a.id - b.id;
|
||||
- }
|
||||
- });
|
||||
+ self.payment_methods = payment_methods;
|
||||
self.payment_methods_by_id = {};
|
||||
_.each(self.payment_methods, function(payment_method) {
|
||||
self.payment_methods_by_id[payment_method.id] = payment_method;
|
||||
21
pos_usability/views/pos_config.xml
Normal file
21
pos_usability/views/pos_config.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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_pos_config_tree" model="ir.ui.view">
|
||||
<field name="model">pos.config</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_config_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="before">
|
||||
<field name="sequence" widget="handle"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
21
pos_usability/views/pos_order.xml
Normal file
21
pos_usability/views/pos_order.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Copyright 2023 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_pos_pos_form" model="ir.ui.view">
|
||||
<field name="model">pos.order</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='lines']/form//field[@name='full_product_name']" position="before">
|
||||
<field name="product_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
20
pos_usability/views/pos_payment_method.xml
Normal file
20
pos_usability/views/pos_payment_method.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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="pos_payment_method_view_tree" model="ir.ui.view">
|
||||
<field name="model">pos.payment.method</field>
|
||||
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="before">
|
||||
<field name="sequence" widget="handle"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
20
pos_usability/views/stock_warehouse.xml
Normal file
20
pos_usability/views/stock_warehouse.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2024 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_warehouse" model="ir.ui.view">
|
||||
<field name="model">stock.warehouse</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="out_type_id" position="after">
|
||||
<field name="pos_type_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,20 +1,50 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * product_category_tax
|
||||
# * product_category_tax
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 8.0-20150727\n"
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-12-04 17:16+0000\n"
|
||||
"PO-Revision-Date: 2015-12-04 17:16+0000\n"
|
||||
"Last-Translator: <>\n"
|
||||
"POT-Creation-Date: 2023-10-16 20:34+0000\n"
|
||||
"PO-Revision-Date: 2023-10-16 20:34+0000\n"
|
||||
"Last-Translator: Alexis de Lattre <alexis.delattre@akretion.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model,name:product_category_tax.model_product_categ_tax_mixin
|
||||
msgid "Common code for taxes on product categories"
|
||||
msgstr "Code commun pour les taxes sur les catégories d'articles"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_categ_tax_mixin__display_name
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__display_name
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_product__display_name
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_template__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom affiché"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_categ_tax_mixin__id
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__id
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_product__id
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_template__id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_categ_tax_mixin____last_update
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_category____last_update
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_product____last_update
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_template____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model,name:product_category_tax.model_product_product
|
||||
msgid "Product"
|
||||
@@ -23,7 +53,7 @@ msgstr "Article"
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model,name:product_category_tax.model_product_category
|
||||
msgid "Product Category"
|
||||
msgstr "Catégorie d'articles"
|
||||
msgstr "Catégorie d'article"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: model:ir.model,name:product_category_tax.model_product_template
|
||||
@@ -31,23 +61,31 @@ msgid "Product Template"
|
||||
msgstr "Modèle d'article"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: field:product.category,purchase_tax_ids:0
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__purchase_tax_ids
|
||||
msgid "Purchase Taxes"
|
||||
msgstr "Taxes fournisseurs"
|
||||
msgstr "Taxes à l'achat"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: field:product.category,sale_tax_ids:0
|
||||
#: model:ir.model.fields,field_description:product_category_tax.field_product_category__sale_tax_ids
|
||||
msgid "Sale Taxes"
|
||||
msgstr "Taxes à la vente"
|
||||
|
||||
#. module: product_category_tax
|
||||
#: code:addons/product_category_tax/product.py:57
|
||||
#: code:addons/product_category_tax/product.py:0
|
||||
#, python-format
|
||||
msgid "The purchase taxes configured on the product '%s' are not the same as the purchase taxes configured on it's related product category '%s'."
|
||||
msgstr "Les taxes fournisseurs paramétrées sur l'article '%s' ne sont pas les mêmes que les taxes fournisseurs paramétrées sur la catégorie interne '%s'."
|
||||
msgid ""
|
||||
"The purchase taxes configured on the product '%s' are not the same as the "
|
||||
"purchase taxes configured on it's related internal category '%s'."
|
||||
msgstr ""
|
||||
"Les taxes à l'achat configurées sur l'article '%s' ne sont pas les mêmes que "
|
||||
"les taxes à l'achat configurées sur sa catégorie interne '%s'."
|
||||
|
||||
#. module: product_category_tax
|
||||
#: code:addons/product_category_tax/product.py:49
|
||||
#: code:addons/product_category_tax/product.py:0
|
||||
#, python-format
|
||||
msgid "The sale taxes configured on the product '%s' are not the same as the sale taxes configured on it's related product category '%s'."
|
||||
msgstr "Les taxes à la vente paramétrées sur l'article '%s' ne sont pas les mêmes que les taxes à la vente paramétrées sur la catégorie interne '%s'."
|
||||
msgid ""
|
||||
"The sale taxes configured on the product '%s' are not the same as the sale "
|
||||
"taxes configured on it's related internal category '%s'."
|
||||
msgstr ""
|
||||
"Les taxes à la vente configurées sur l'article '%s' ne sont pas les mêmes que "
|
||||
"les taxes à la vente configurées sur sa catégorie interne '%s'."
|
||||
|
||||
@@ -10,4 +10,4 @@ class ProductTemplate(models.Model):
|
||||
|
||||
detailed_type = fields.Selection(selection_add=[
|
||||
('product', 'Storable Product')
|
||||
], ondelete={'product': 'set default'})
|
||||
], ondelete={'product': 'set default'}, default='product')
|
||||
|
||||
@@ -32,7 +32,7 @@ This module has been written by Alexis de Lattre from Akretion
|
||||
<alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
# We depend on point_of_sale and not only 'product'
|
||||
# because the price barcode rule is added by the point_of_sale module
|
||||
# (the weight barcode rule is added by the stock module)
|
||||
@@ -46,6 +46,7 @@ This module has been written by Alexis de Lattre from Akretion
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/product_print_zpl_barcode_view.xml',
|
||||
'views/product.xml',
|
||||
'views/stock_picking.xml',
|
||||
'data/barcode_sequence.xml',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
412
product_print_zpl_barcode/i18n/fr.po
Normal file
412
product_print_zpl_barcode/i18n/fr.po
Normal file
@@ -0,0 +1,412 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * product_print_zpl_barcode
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-07-15 13:39+0000\n"
|
||||
"PO-Revision-Date: 2023-07-15 13:39+0000\n"
|
||||
"Last-Translator: \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: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__copies
|
||||
msgid "# Labels"
|
||||
msgstr "Nb étiquettes"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields.selection,name:product_print_zpl_barcode.selection__product_print_zpl_barcode__label_size__38x25
|
||||
msgid "38x25 mm"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__barcode
|
||||
msgid "Barcode"
|
||||
msgstr "Code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__nomenclature_id
|
||||
msgid "Barcode Nomenclature"
|
||||
msgstr "Nomenclature des codes-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__rule_id
|
||||
msgid "Barcode Rule"
|
||||
msgstr "Règle de codes-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__barcode_type
|
||||
msgid "Barcode Type"
|
||||
msgstr "Type de code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__company_id
|
||||
msgid "Company"
|
||||
msgstr "Société"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__create_uid
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Créé par"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__create_date
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Créé le"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__currency_id
|
||||
msgid "Currency"
|
||||
msgstr "Devise"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,help:product_print_zpl_barcode.field_product_print_zpl_barcode_line__uom_id
|
||||
msgid "Default unit of measure used for all stock operations."
|
||||
msgstr ""
|
||||
"Unité de mesure par défaut utilisée pour toutes les opérations de stock."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__display_name
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Nom affiché"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,help:product_print_zpl_barcode.field_product_product__must_print_barcode
|
||||
#: model:ir.model.fields,help:product_print_zpl_barcode.field_product_template__must_print_barcode
|
||||
msgid ""
|
||||
"Enable that option for products for which you must print a barcode upon "
|
||||
"reception in stock."
|
||||
msgstr ""
|
||||
"Activez cette option sur les articles pour lesquels vous devez imprimer un "
|
||||
"code-barres dès leur réception en stock."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.actions.act_window,name:product_print_zpl_barcode.product_print_zpl_barcode_action
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_normal_form_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_template_only_form_view
|
||||
msgid "Generate Barcode"
|
||||
msgstr "Générer un code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Generate Labels"
|
||||
msgstr "Générer les étiquettes"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_print_zpl_barcode
|
||||
msgid "Generate and print product barcodes in ZPL"
|
||||
msgstr "Générer et imprimer des codes-barres d'articles en ZPL"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__id
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__label_size
|
||||
msgid "Label Size"
|
||||
msgstr "Taille de l'étiquette"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode____last_update
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__write_uid
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Dernière mise à jour par"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__write_date
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Dernière mise à jour le"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Line '%s': barcode '%s' has %d digits. This wizard only supports EAN8 and "
|
||||
"EAN13 for the moment."
|
||||
msgstr ""
|
||||
"Ligne '%s' : le code-barres '%s' comporte %d chiffres. Cet assistant ne "
|
||||
"prend en charge que les EAN8 et EAN13 pour le moment."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "Line '%s': barcode type '%s' is not supported for the moment"
|
||||
msgstr ""
|
||||
"Ligne '%s' : le type de code-barres '%s' n'est pas supporté pour le moment"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Line '%s': the barcode '%s' is not a valid EAN barcode (wrong checksum)."
|
||||
msgstr ""
|
||||
"Ligne '%s' : le code-barres '%s' n'est pas un code-barres EAN valide "
|
||||
"(mauvaise somme de contrôle)."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_print_zpl_barcode_line
|
||||
msgid "Line of the print ZPL barcode wizard"
|
||||
msgstr "Ligne de l'assistant d'impression du code-barres ZPL"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__line_ids
|
||||
msgid "Lines"
|
||||
msgstr "Lignes"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "Missing Products"
|
||||
msgstr "Produits manquants"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_product__must_print_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_template__must_print_barcode
|
||||
msgid "Must Print Barcode"
|
||||
msgstr "Code-barres à imprimer"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "On line '%s', the number of copies must be strictly positive."
|
||||
msgstr ""
|
||||
"Sur la ligne '%s', le nombre d'étiquettes doit être strictement positif."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_product__barcode_image_png
|
||||
msgid "PNG Barcode Image"
|
||||
msgstr "Image PNG du code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__parent_id
|
||||
msgid "Parent"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__price
|
||||
msgid "Price"
|
||||
msgstr "Prix"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__price_uom
|
||||
msgid "Price/UoM"
|
||||
msgstr "Prix/unité"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__pricelist_id
|
||||
msgid "Pricelist"
|
||||
msgstr "Liste de prix"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Print"
|
||||
msgstr "Imprimer"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_normal_form_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_template_only_form_view
|
||||
msgid "Print Barcode"
|
||||
msgstr "Imprimer le code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_product_tree_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_template_tree_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.view_picking_form
|
||||
msgid "Print Barcodes"
|
||||
msgstr "Imprimer les code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_template
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__product_id
|
||||
msgid "Product"
|
||||
msgstr "Article"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__product_name
|
||||
msgid "Product Label"
|
||||
msgstr "Étiquette de l'article"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_product
|
||||
msgid "Product Variant"
|
||||
msgstr "Variante d'article"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__quantity
|
||||
msgid "Qty"
|
||||
msgstr "Qté"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_product__barcode_image_svg
|
||||
msgid "SVG Barcode Image"
|
||||
msgstr "Image SVG du code-barres"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_stock_picking__show_print_zpl_barcode
|
||||
msgid "Show Print Zpl Barcode"
|
||||
msgstr "Afficher le bouton imprimer le code-barres ZPL"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__state
|
||||
msgid "State"
|
||||
msgstr "État"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields.selection,name:product_print_zpl_barcode.selection__product_print_zpl_barcode__state__step1
|
||||
msgid "Step1"
|
||||
msgstr "Étape 1"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields.selection,name:product_print_zpl_barcode.selection__product_print_zpl_barcode__state__step2
|
||||
msgid "Step2"
|
||||
msgstr "Étape 2"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The barcode of the product (%s) has %d characters, which is smaller than the"
|
||||
" %d characters of the prefix of the barcode pattern (%s)."
|
||||
msgstr ""
|
||||
"Le code-barres de l'article (%s) comporte %d caractères, ce qui est plus "
|
||||
"petit que les %d caractères du préfixe du modèle de code-barres (%s)."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The barcode rule '%s' has a pattern '%s' which doesn't contain a integer and"
|
||||
" decimal part between '{}'."
|
||||
msgstr ""
|
||||
"La règle de code-barres '%s' a un motif '%s' qui ne contient pas de partie "
|
||||
"entière et décimale entre '{}'."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/models/product.py:0
|
||||
#, python-format
|
||||
msgid "The product '%s' already has a barcode."
|
||||
msgstr "L'article '%s' a déjà un code-barres."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "The quantity (%s) must be positive !"
|
||||
msgstr "La quantité (%s) doit être positive !"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/models/product.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The sequence 'private.product.barcode' is not properly configured. The "
|
||||
"generated sequence should have 7 digits (for EAN-8) or 12 digits (for "
|
||||
"EAN-13). It currently has %d digits."
|
||||
msgstr ""
|
||||
"La séquence 'private.product.barcode' n'est pas correctement configurée. La "
|
||||
"séquence générée devrait avoir 7 chiffres (pour EAN-8) ou 12 chiffres (pour "
|
||||
"EAN-13). Elle comporte actuellement %d chiffres."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The value to encode in the barcode (%s) is superior to the maximum value "
|
||||
"allowed by the barcode pattern (%s)."
|
||||
msgstr ""
|
||||
"La valeur à encoder dans le code-barres (%s) est supérieure à la valeur "
|
||||
"maximale autorisée par le motif du code-barres (%s)."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "There are no pricelist in company '%s'."
|
||||
msgstr "Il n'y a pas de liste de prix dans la société '%s'."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_stock_picking
|
||||
msgid "Transfer"
|
||||
msgstr "Transfert"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__uom_id
|
||||
msgid "UoM"
|
||||
msgstr "Unité"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "Wrong active_model in context (%s)."
|
||||
msgstr "Mauvais active_model dans le contexte (%s)."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/models/product.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You cannot call the method generate_barcode_from_product_template on product"
|
||||
" '%s' because it has %d variants and not just one."
|
||||
msgstr ""
|
||||
"Vous ne pouvez pas appeler la méthode generate_barcode_from_product_template"
|
||||
" sur l'article '%s' parce qu'il a %d variantes et non une seule."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "You must select a ZPL Printer."
|
||||
msgstr "Vous devez sélectionner une imprimante ZPL."
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__zpl_file
|
||||
msgid "ZPL File"
|
||||
msgstr "Fichier ZPL"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__zpl_filename
|
||||
msgid "ZPL Filename"
|
||||
msgstr "Nom du fichier ZPL"
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__zpl_printer_id
|
||||
msgid "ZPL Printer"
|
||||
msgstr "Imprimante ZPL"
|
||||
392
product_print_zpl_barcode/i18n/product_print_zpl_barcode.pot
Normal file
392
product_print_zpl_barcode/i18n/product_print_zpl_barcode.pot
Normal file
@@ -0,0 +1,392 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * product_print_zpl_barcode
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-07-15 13:39+0000\n"
|
||||
"PO-Revision-Date: 2023-07-15 13:39+0000\n"
|
||||
"Last-Translator: \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: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__copies
|
||||
msgid "# Labels"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields.selection,name:product_print_zpl_barcode.selection__product_print_zpl_barcode__label_size__38x25
|
||||
msgid "38x25 mm"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__barcode
|
||||
msgid "Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__nomenclature_id
|
||||
msgid "Barcode Nomenclature"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__rule_id
|
||||
msgid "Barcode Rule"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__barcode_type
|
||||
msgid "Barcode Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__create_uid
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__create_date
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__currency_id
|
||||
msgid "Currency"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,help:product_print_zpl_barcode.field_product_print_zpl_barcode_line__uom_id
|
||||
msgid "Default unit of measure used for all stock operations."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__display_name
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,help:product_print_zpl_barcode.field_product_product__must_print_barcode
|
||||
#: model:ir.model.fields,help:product_print_zpl_barcode.field_product_template__must_print_barcode
|
||||
msgid ""
|
||||
"Enable that option for products for which you must print a barcode upon "
|
||||
"reception in stock."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.actions.act_window,name:product_print_zpl_barcode.product_print_zpl_barcode_action
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_normal_form_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_template_only_form_view
|
||||
msgid "Generate Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Generate Labels"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_print_zpl_barcode
|
||||
msgid "Generate and print product barcodes in ZPL"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__id
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__label_size
|
||||
msgid "Label Size"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode____last_update
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__write_uid
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__write_date
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Line '%s': barcode '%s' has %d digits. This wizard only supports EAN8 and "
|
||||
"EAN13 for the moment."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "Line '%s': barcode type '%s' is not supported for the moment"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Line '%s': the barcode '%s' is not a valid EAN barcode (wrong checksum)."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_print_zpl_barcode_line
|
||||
msgid "Line of the print ZPL barcode wizard"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__line_ids
|
||||
msgid "Lines"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "Missing Products"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_product__must_print_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_template__must_print_barcode
|
||||
msgid "Must Print Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "On line '%s', the number of copies must be strictly positive."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_product__barcode_image_png
|
||||
msgid "PNG Barcode Image"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__parent_id
|
||||
msgid "Parent"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__price
|
||||
msgid "Price"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__price_uom
|
||||
msgid "Price/UoM"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__pricelist_id
|
||||
msgid "Pricelist"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_print_zpl_barcode_form
|
||||
msgid "Print"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_normal_form_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_template_only_form_view
|
||||
msgid "Print Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_product_tree_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.product_template_tree_view
|
||||
#: model_terms:ir.ui.view,arch_db:product_print_zpl_barcode.view_picking_form
|
||||
msgid "Print Barcodes"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_template
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__product_id
|
||||
msgid "Product"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__product_name
|
||||
msgid "Product Label"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_product_product
|
||||
msgid "Product Variant"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__quantity
|
||||
msgid "Qty"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_product__barcode_image_svg
|
||||
msgid "SVG Barcode Image"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_stock_picking__show_print_zpl_barcode
|
||||
msgid "Show Print Zpl Barcode"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__state
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields.selection,name:product_print_zpl_barcode.selection__product_print_zpl_barcode__state__step1
|
||||
msgid "Step1"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields.selection,name:product_print_zpl_barcode.selection__product_print_zpl_barcode__state__step2
|
||||
msgid "Step2"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The barcode of the product (%s) has %d characters, which is smaller than the"
|
||||
" %d characters of the prefix of the barcode pattern (%s)."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The barcode rule '%s' has a pattern '%s' which doesn't contain a integer and"
|
||||
" decimal part between '{}'."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/models/product.py:0
|
||||
#, python-format
|
||||
msgid "The product '%s' already has a barcode."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "The quantity (%s) must be positive !"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/models/product.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The sequence 'private.product.barcode' is not properly configured. The "
|
||||
"generated sequence should have 7 digits (for EAN-8) or 12 digits (for "
|
||||
"EAN-13). It currently has %d digits."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The value to encode in the barcode (%s) is superior to the maximum value "
|
||||
"allowed by the barcode pattern (%s)."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "There are no pricelist in company '%s'."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model,name:product_print_zpl_barcode.model_stock_picking
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode_line__uom_id
|
||||
msgid "UoM"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "Wrong active_model in context (%s)."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/models/product.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You cannot call the method generate_barcode_from_product_template on product"
|
||||
" '%s' because it has %d variants and not just one."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#. odoo-python
|
||||
#: code:addons/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py:0
|
||||
#, python-format
|
||||
msgid "You must select a ZPL Printer."
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__zpl_file
|
||||
msgid "ZPL File"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__zpl_filename
|
||||
msgid "ZPL Filename"
|
||||
msgstr ""
|
||||
|
||||
#. module: product_print_zpl_barcode
|
||||
#: model:ir.model.fields,field_description:product_print_zpl_barcode.field_product_print_zpl_barcode__zpl_printer_id
|
||||
msgid "ZPL Printer"
|
||||
msgstr ""
|
||||
@@ -1 +1,2 @@
|
||||
from . import product
|
||||
from . import stock_picking
|
||||
|
||||
@@ -29,22 +29,6 @@ class ProductTemplate(models.Model):
|
||||
% (self.display_name, self.product_variant_count))
|
||||
return self.product_variant_ids[0].generate_barcode_from_product_product()
|
||||
|
||||
def print_zpl_barcode_from_product_template(self):
|
||||
self.ensure_one()
|
||||
if self.product_variant_count != 1:
|
||||
raise UserError(_(
|
||||
"You cannot call the method "
|
||||
"print_zpl_barcode_from_product_template on product '%s' "
|
||||
"because it has %d variants and not just one.")
|
||||
% (self.display_name, self.product_variant_count))
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
'product_print_zpl_barcode.product_print_zpl_barcode_action')
|
||||
action['context'] = {
|
||||
'active_id': self.product_variant_ids[0].id,
|
||||
'active_model': 'product.product',
|
||||
}
|
||||
return action
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
25
product_print_zpl_barcode/models/stock_picking.py
Normal file
25
product_print_zpl_barcode/models/stock_picking.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright 2023 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
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
show_print_zpl_barcode = fields.Boolean(compute='_compute_show_print_zpl_barcode')
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_show_print_zpl_barcode(self):
|
||||
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
for picking in self:
|
||||
show = False
|
||||
if picking.state == 'done' and picking.picking_type_code != 'outgoing':
|
||||
for line in picking.move_line_ids:
|
||||
if (
|
||||
line.product_id.must_print_barcode and
|
||||
float_compare(line.qty_done, 0, precision_digits=prec) > 0):
|
||||
show = True
|
||||
picking.show_print_zpl_barcode = show
|
||||
@@ -1,2 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_product_print_zpl_barcode,Full access to product.print.zpl.barcode wizard,model_product_print_zpl_barcode,base_report_to_printer.printing_group_user,1,1,1,1
|
||||
access_product_print_zpl_barcode_line,Full access to product.print.zpl.barcode.line wizard,model_product_print_zpl_barcode_line,base_report_to_printer.printing_group_user,1,1,1,1
|
||||
|
||||
|
@@ -27,11 +27,23 @@
|
||||
<field name="arch" type="xml">
|
||||
<header position="inside">
|
||||
<button name="generate_barcode_from_product_template" type="object" string="Generate Barcode" attrs="{'invisible': ['|', ('product_variant_count', '>', 1), ('barcode', '!=', False)]}"/>
|
||||
<button name="print_zpl_barcode_from_product_template" type="object" string="Print Barcode" groups="base_report_to_printer.printing_group_user" attrs="{'invisible': ['|', ('product_variant_count', '>', 1), ('barcode', '=', False)]}"/>
|
||||
<button name="%(product_print_zpl_barcode.product_print_zpl_barcode_action)d" type="action" string="Print Barcode" groups="base_report_to_printer.printing_group_user" attrs="{'invisible': ['|', ('product_variant_count', '>', 1), ('barcode', '=', False)]}"/>
|
||||
</header>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_tree_view" model="ir.ui.view">
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_variant_count" position="before">
|
||||
<header>
|
||||
<button name="%(product_print_zpl_barcode.product_print_zpl_barcode_action)d" type="action" string="Print Barcodes" groups="base_report_to_printer.printing_group_user"/>
|
||||
</header>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="product_normal_form_view" model="ir.ui.view">
|
||||
<field name="model">product.product</field>
|
||||
@@ -44,5 +56,16 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_tree_view" model="ir.ui.view">
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_product_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="default_code" position="before">
|
||||
<header>
|
||||
<button name="%(product_print_zpl_barcode.product_print_zpl_barcode_action)d" type="action" string="Print Barcodes" groups="base_report_to_printer.printing_group_user"/>
|
||||
</header>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
21
product_print_zpl_barcode/views/stock_picking.xml
Normal file
21
product_print_zpl_barcode/views/stock_picking.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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_picking_form" model="ir.ui.view">
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_toggle_is_locked" position="after">
|
||||
<button name="%(product_print_zpl_barcode.product_print_zpl_barcode_action)d" type="action" string="Print Barcodes" attrs="{'invisible': [('show_print_zpl_barcode', '=', False)]}"/>
|
||||
<field name="show_print_zpl_barcode" invisible="1"/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2016-2020 Akretion France (http://www.akretion.com/)
|
||||
# Copyright 2016-2023 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,18 @@ from stdnum.ean import is_valid, calc_check_digit
|
||||
import base64
|
||||
import re
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProductPrintZplBarcode(models.TransientModel):
|
||||
_name = 'product.print.zpl.barcode'
|
||||
_description = 'Generate and print product barcodes in ZPL'
|
||||
_check_company_auto = True
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
assert self._context.get('active_model') == 'product.product',\
|
||||
'wrong active_model, should be product.product'
|
||||
product_id = self._context.get('active_id')
|
||||
product = self.env['product.product'].browse(product_id)
|
||||
if not product:
|
||||
raise UserError(_('Missing Product'))
|
||||
if not product.barcode:
|
||||
raise UserError(_(
|
||||
"Product '%s' doesn't have a barcode") % product.display_name)
|
||||
nomenclature = self.env.ref('barcodes.default_barcode_nomenclature')
|
||||
company = self.env.company
|
||||
posconfig = self.env['pos.config'].sudo().search(
|
||||
@@ -39,82 +34,200 @@ class ProductPrintZplBarcode(models.TransientModel):
|
||||
], limit=1)
|
||||
if not pricelist:
|
||||
raise UserError(_(
|
||||
"There are no pricelist in company %s ?") % company.name)
|
||||
"There are no pricelist in company '%s'.") % company.name)
|
||||
|
||||
printer = self.env['printing.printer'].get_default()
|
||||
|
||||
line_ids = []
|
||||
if self._context.get('active_model') == 'product.product':
|
||||
product_ids = self._context.get('active_ids')
|
||||
products = self.env['product.product'].browse(product_ids)
|
||||
if not products:
|
||||
raise UserError(_('Missing Products'))
|
||||
for product in products:
|
||||
self._update_line_ids(line_ids, product)
|
||||
elif self._context.get('active_model') == 'product.template':
|
||||
product_tmpl_ids = self._context.get('active_ids')
|
||||
product_tmpls = self.env['product.template'].browse(product_tmpl_ids)
|
||||
for product_tmpl in product_tmpls:
|
||||
for product in product_tmpl.product_variant_ids:
|
||||
self._update_line_ids(line_ids, product)
|
||||
elif self._context.get('active_model') == 'stock.picking':
|
||||
prec = self.env['decimal.precision'].precision_get(
|
||||
'Product Unit of Measure')
|
||||
picking = self.env['stock.picking'].browse(self._context['active_id'])
|
||||
for ml in picking.move_line_ids:
|
||||
if (
|
||||
ml.product_id and
|
||||
ml.product_id.must_print_barcode and
|
||||
float_compare(ml.qty_done, 0, precision_digits=prec) > 0):
|
||||
self._update_line_ids(
|
||||
line_ids, ml.product_id, int(round(ml.qty_done)))
|
||||
else:
|
||||
raise UserError(_(
|
||||
"Wrong active_model in context (%s).")
|
||||
% self._context.get('active_model'))
|
||||
res.update({
|
||||
'company_id': company.id,
|
||||
'nomenclature_id': nomenclature.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'currency_id': pricelist.currency_id.id,
|
||||
'barcode': product.barcode,
|
||||
'product_name': product.name,
|
||||
'product_id': product_id,
|
||||
'zpl_printer_id': printer and printer.id or False,
|
||||
'line_ids': line_ids,
|
||||
})
|
||||
return res
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product', string='Product', required=True, readonly=True)
|
||||
uom_id = fields.Many2one(related='product_id.uom_id')
|
||||
# 1 line = un peu moins de 30
|
||||
product_name = fields.Char('Product Label', required=True, size=56)
|
||||
@api.model
|
||||
def _update_line_ids(self, line_ids, product, copies=1):
|
||||
if product.barcode:
|
||||
line_ids.append((0, 0, {
|
||||
'barcode': product.barcode,
|
||||
'product_name': product.name,
|
||||
'product_id': product.id,
|
||||
'copies': copies,
|
||||
}))
|
||||
else:
|
||||
logger.warning("Product '%s' doesn't have a barcode", product.display_name)
|
||||
|
||||
company_id = fields.Many2one( # default value set by default_get
|
||||
'res.company', required=True, ondelete='cascade')
|
||||
nomenclature_id = fields.Many2one(
|
||||
'barcode.nomenclature', 'Barcode Nomenclature', required=True)
|
||||
rule_id = fields.Many2one(
|
||||
'barcode.rule', string='Barcode Rule', readonly=True,
|
||||
compute='_compute_rule_id')
|
||||
barcode_type = fields.Selection(related='rule_id.type', string="Barcode Type")
|
||||
'barcode.nomenclature', 'Barcode Nomenclature', required=True,
|
||||
states={'step2': [('readonly', True)]})
|
||||
# label_size: remove readonly=True when we will support more labels
|
||||
label_size = fields.Selection([
|
||||
('38x25', '38x25 mm'),
|
||||
], required=True, default='38x25')
|
||||
], required=True, default='38x25', readonly=True)
|
||||
pricelist_id = fields.Many2one(
|
||||
'product.pricelist', string='Pricelist', required=True)
|
||||
currency_id = fields.Many2one(related='pricelist_id.currency_id')
|
||||
# TODO: for the moment, we only support weight, but...
|
||||
quantity = fields.Float(digits='Stock Weight')
|
||||
price_uom = fields.Monetary(
|
||||
readonly=True, string="Price per Unit of Measure",
|
||||
compute='_compute_price') # given by pricelist
|
||||
price = fields.Monetary(compute='_compute_price', readonly=True)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency')
|
||||
'product.pricelist', string='Pricelist', required=True,
|
||||
states={'step2': [('readonly', True)]}, check_company=True,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]"
|
||||
)
|
||||
state = fields.Selection([
|
||||
('step1', 'Step1'),
|
||||
('step2', 'Step2'),
|
||||
], default='step1', readonly=True)
|
||||
zpl_file = fields.Binary(string='ZPL File', readonly=True)
|
||||
zpl_filename = fields.Char('ZPL Filename')
|
||||
barcode = fields.Char(readonly=True)
|
||||
copies = fields.Integer(
|
||||
string='Number of Labels', default=1, required=True)
|
||||
zpl_printer_id = fields.Many2one(
|
||||
'printing.printer', string='ZPL Printer')
|
||||
line_ids = fields.One2many(
|
||||
'product.print.zpl.barcode.line', 'parent_id',
|
||||
string='Lines', states={'step2': [('readonly', True)]})
|
||||
|
||||
@api.depends('pricelist_id', 'quantity', 'product_id')
|
||||
def generate(self):
|
||||
"""Called by button for the wizard, 1st step"""
|
||||
self.ensure_one()
|
||||
zpl_strings = []
|
||||
for line in self.line_ids:
|
||||
barcode = line.barcode
|
||||
product_name = line.product_name
|
||||
assert barcode
|
||||
barcode_len = len(barcode)
|
||||
if barcode_len not in (8, 13):
|
||||
raise UserError(_(
|
||||
"Line '%s': barcode '%s' has %d digits. "
|
||||
"This wizard only supports EAN8 and EAN13 for the moment.")
|
||||
% (product_name, barcode, barcode_len))
|
||||
if not is_valid(barcode):
|
||||
raise UserError(_(
|
||||
"Line '%s': the barcode '%s' is not a valid EAN barcode "
|
||||
"(wrong checksum).") % (product_name, barcode))
|
||||
if line.copies <= 0:
|
||||
raise UserError(_(
|
||||
"On line '%s', the number of copies must be strictly positive."
|
||||
) % product_name)
|
||||
if line.barcode_type in ('price', 'weight'):
|
||||
barcode, zpl_str = line._prepare_price_weight_barcode_type()
|
||||
elif line.barcode_type == 'product':
|
||||
barcode, zpl_str = line._prepare_product_barcode_type()
|
||||
else:
|
||||
raise UserError(_(
|
||||
"Line '%s': barcode type '%s' is not supported for the moment")
|
||||
% (product_name, line.barcode_type))
|
||||
line.write({'barcode': barcode})
|
||||
zpl_strings.append(zpl_str)
|
||||
|
||||
zpl_filename = "barcodes.zpl"
|
||||
if len(self.line_ids) == 1:
|
||||
zpl_filename = "barcode_%s.zpl" % self.line_ids[0].barcode
|
||||
|
||||
zpl_str = '\n'.join(zpl_strings)
|
||||
zpl_bytes = zpl_str.encode('utf-8')
|
||||
vals = {
|
||||
'zpl_file': base64.encodebytes(zpl_bytes),
|
||||
'state': 'step2',
|
||||
'zpl_filename': zpl_filename,
|
||||
}
|
||||
self.write(vals)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
'product_print_zpl_barcode.product_print_zpl_barcode_action')
|
||||
action.update({
|
||||
'res_id': self.id,
|
||||
'context': self._context,
|
||||
'views': False})
|
||||
return action
|
||||
|
||||
def print_zpl(self):
|
||||
if not self.zpl_printer_id:
|
||||
raise UserError(_(
|
||||
"You must select a ZPL Printer."))
|
||||
self.zpl_printer_id.print_document(
|
||||
self.zpl_filename, base64.decodebytes(self.zpl_file), format='raw')
|
||||
|
||||
|
||||
class ProductPrintZplBarcodeLine(models.TransientModel):
|
||||
_name = 'product.print.zpl.barcode.line'
|
||||
_description = 'Line of the print ZPL barcode wizard'
|
||||
|
||||
parent_id = fields.Many2one(
|
||||
'product.print.zpl.barcode', ondelete='cascade')
|
||||
product_id = fields.Many2one(
|
||||
'product.product', string='Product', readonly=True)
|
||||
uom_id = fields.Many2one(related='product_id.uom_id', string='UoM')
|
||||
# 1 line = a bit less than 30
|
||||
# I don't make product_name a stored computed field because I'm afraid
|
||||
# that we may not take the lang of the user
|
||||
product_name = fields.Char('Product Label', required=True, size=56)
|
||||
rule_id = fields.Many2one(
|
||||
'barcode.rule', string='Barcode Rule', compute='_compute_rule_id')
|
||||
barcode_type = fields.Selection(related='rule_id.type', string="Barcode Type")
|
||||
currency_id = fields.Many2one(related='parent_id.pricelist_id.currency_id')
|
||||
# TODO: for the moment, we only support weight, but...
|
||||
quantity = fields.Float(digits='Stock Weight', string='Qty')
|
||||
price_uom = fields.Monetary(
|
||||
string="Price/UoM", compute='_compute_price') # given by pricelist
|
||||
price = fields.Monetary(compute='_compute_price')
|
||||
barcode = fields.Char(readonly=True)
|
||||
copies = fields.Integer(string='# Labels', default=1, required=True)
|
||||
|
||||
@api.depends('parent_id.pricelist_id', 'quantity', 'product_id')
|
||||
def _compute_price(self):
|
||||
# for regular barcodes
|
||||
for wiz in self:
|
||||
if wiz.pricelist_id and wiz.product_id:
|
||||
price_uom = wiz.pricelist_id.get_product_price(
|
||||
wiz.product_id, 1, False)
|
||||
wiz.price_uom = price_uom
|
||||
wiz.price = price_uom * wiz.quantity
|
||||
for line in self:
|
||||
pricelist = line.parent_id.pricelist_id
|
||||
price_uom = price = 0.0
|
||||
if pricelist and line.product_id:
|
||||
price_uom = pricelist.get_product_price(line.product_id, 1, False)
|
||||
price = price_uom * line.quantity
|
||||
line.price_uom = price_uom
|
||||
line.price = price
|
||||
|
||||
@api.depends('nomenclature_id')
|
||||
@api.depends('parent_id.nomenclature_id')
|
||||
def _compute_rule_id(self):
|
||||
for wiz in self:
|
||||
for line in self:
|
||||
nomenclature = line.parent_id.nomenclature_id
|
||||
match_rule = False
|
||||
if wiz.nomenclature_id and wiz.barcode:
|
||||
for rule in wiz.nomenclature_id.rule_ids:
|
||||
match = wiz.nomenclature_id.match_pattern(
|
||||
wiz.barcode, rule.pattern)
|
||||
if nomenclature and line.barcode:
|
||||
for rule in nomenclature.rule_ids:
|
||||
match = nomenclature.match_pattern(
|
||||
line.barcode, rule.pattern)
|
||||
if match.get('match'):
|
||||
match_rule = rule.id
|
||||
break
|
||||
wiz.rule_id = match_rule
|
||||
line.rule_id = match_rule
|
||||
|
||||
def _prepare_price_weight_barcode_type(self):
|
||||
dpo = self.env['decimal.precision']
|
||||
bno = self.env['barcode.nomenclature']
|
||||
prec = dpo.precision_get('Stock Weight')
|
||||
value = self.quantity
|
||||
pbarcode = self.barcode
|
||||
@@ -139,7 +252,7 @@ class ProductPrintZplBarcode(models.TransientModel):
|
||||
barcode = pbarcode[0:len(prefix)]
|
||||
# print("barcode=", barcode)
|
||||
# print("pattern=", pattern)
|
||||
m = re.search('\{N+D+\}', pattern)
|
||||
m = re.search(r'\{N+D+\}', pattern)
|
||||
# print("m=", m)
|
||||
assert m
|
||||
pattern_val = m.group(0)
|
||||
@@ -172,7 +285,7 @@ class ProductPrintZplBarcode(models.TransientModel):
|
||||
assert len(barcode) == 13
|
||||
assert is_valid(barcode)
|
||||
# print("barcode FINAL=", barcode)
|
||||
zpl_unicode = self._price_weight_barcode_type_zpl() % {
|
||||
zpl_str = self._price_weight_barcode_type_zpl() % {
|
||||
'product_name': self.product_name,
|
||||
'ean_zpl_command': len(self.barcode) == 8 and 'B8' or 'BE',
|
||||
'ean_no_checksum': barcode[:-1],
|
||||
@@ -183,12 +296,7 @@ class ProductPrintZplBarcode(models.TransientModel):
|
||||
'quantity': value,
|
||||
'uom_name': self.uom_id.name,
|
||||
}
|
||||
zpl_bytes = zpl_unicode.encode('utf-8')
|
||||
vals = {
|
||||
'zpl_file': base64.encodebytes(zpl_bytes),
|
||||
'barcode': barcode,
|
||||
}
|
||||
return vals
|
||||
return (barcode, zpl_str)
|
||||
|
||||
@api.model
|
||||
def _price_weight_barcode_type_zpl(self):
|
||||
@@ -229,7 +337,7 @@ class ProductPrintZplBarcode(models.TransientModel):
|
||||
return label
|
||||
|
||||
def _prepare_product_barcode_type(self):
|
||||
zpl_unicode = self._product_barcode_type_zpl() % {
|
||||
zpl_str = self._product_barcode_type_zpl() % {
|
||||
'product_name': self.product_name,
|
||||
'ean_zpl_command': len(self.barcode) == 8 and 'B8' or 'BE',
|
||||
'ean_no_checksum': self.barcode[:-1],
|
||||
@@ -237,60 +345,4 @@ class ProductPrintZplBarcode(models.TransientModel):
|
||||
'currency_symbol': self.currency_id.symbol, # symbol is a required field
|
||||
'copies': self.copies,
|
||||
}
|
||||
zpl_bytes = zpl_unicode.encode('utf-8')
|
||||
vals = {
|
||||
'zpl_file': base64.encodebytes(zpl_bytes),
|
||||
'barcode': self.barcode, # unchanged
|
||||
}
|
||||
return vals
|
||||
|
||||
def generate(self):
|
||||
assert self.barcode
|
||||
if len(self.barcode) not in (8, 13):
|
||||
raise UserError(_(
|
||||
"This wizard only supports EAN8 and EAN13 for the moment. "
|
||||
"Barcode '%s' has %d digits.") % (
|
||||
self.barcode,
|
||||
len(self.barcode)))
|
||||
if not is_valid(self.barcode):
|
||||
raise UserError(_(
|
||||
"The barcode '%s' is not a valid EAN barcode "
|
||||
"(wrong checksum).") % self.barcode)
|
||||
if not self.copies:
|
||||
raise UserError(_("The number of copies cannot be 0"))
|
||||
if self.barcode_type in ('price', 'weight'):
|
||||
vals = self._prepare_price_weight_barcode_type()
|
||||
elif self.barcode_type == 'product':
|
||||
vals = self._prepare_product_barcode_type()
|
||||
else:
|
||||
raise UserError(_(
|
||||
"Barcode Type %s is not supported for the moment")
|
||||
% self.barcode_type)
|
||||
vals.update({
|
||||
'state': 'step2',
|
||||
'zpl_filename': 'barcode_%s.zpl' % vals['barcode'],
|
||||
})
|
||||
self.write(vals)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
'product_print_zpl_barcode.product_print_zpl_barcode_action')
|
||||
action.update({
|
||||
'res_id': self.id,
|
||||
'context': self._context,
|
||||
'views': False})
|
||||
return action
|
||||
|
||||
def print_zpl(self):
|
||||
if not self.zpl_printer_id:
|
||||
raise UserError(_(
|
||||
"You must select a ZPL Printer."))
|
||||
self.zpl_printer_id.print_document(
|
||||
self.zpl_filename, base64.decodebytes(self.zpl_file), format='raw')
|
||||
action = True
|
||||
if self._context.get('print_and_new'):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
'product_print_zpl_barcode.product_print_zpl_barcode_action')
|
||||
action.update({
|
||||
'views': False,
|
||||
'context': self._context,
|
||||
})
|
||||
return action
|
||||
return (self.barcode, zpl_str)
|
||||
|
||||
@@ -11,38 +11,41 @@
|
||||
<field name="name">product_print_zpl_barcode.form</field>
|
||||
<field name="model">product.print.zpl.barcode</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Generate and Print Product Barcode">
|
||||
<group name="step1" string="Configuration">
|
||||
<form>
|
||||
<group name="step1">
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="product_id"/>
|
||||
<field name="product_name" attrs="{'readonly': [('state', '=', 'step2')]}"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="pricelist_id" attrs="{'readonly': [('state', '=', 'step2')]}"/>
|
||||
<field name="price_uom"/>
|
||||
<field name="label_size" attrs="{'readonly': [('state', '=', 'step2')]}"/>
|
||||
<field name="nomenclature_id" attrs="{'readonly': [('state', '=', 'step2')]}"/>
|
||||
<field name="rule_id"/>
|
||||
<field name="barcode_type"/>
|
||||
<field name="barcode"/>
|
||||
<field name="copies" attrs="{'readonly': [('state', '=', 'step2')]}"/>
|
||||
</group>
|
||||
<group string="Enter Quantity" attrs="{'invisible': [('barcode_type', '=', 'product')]}">
|
||||
<div name="qty_uom">
|
||||
<field name="quantity" attrs="{'readonly': [('state', '=', 'step2')]}" class="oe_inline"/>
|
||||
<field name="uom_id" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
<group name="step2" states="step2" string="Label">
|
||||
<field name="price" attrs="{'invisible': [('barcode_type', 'not in', ('price', 'weight'))]}"/>
|
||||
<group name="step2" states="step2">
|
||||
<field name="zpl_file" filename="zpl_filename" />
|
||||
<field name="zpl_filename" invisible="1"/>
|
||||
<field name="zpl_printer_id" attrs="{'required': [('state', '=', 'step2')]}"/>
|
||||
</group>
|
||||
<group name="lines">
|
||||
<field name="line_ids" colspan="2" nolabel="1">
|
||||
<tree editable="bottom">
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="product_id" optional="hide" force_save="1"/>
|
||||
<field name="product_name"/>
|
||||
<field name="price_uom"/>
|
||||
<field name="rule_id" optional="show"/>
|
||||
<field name="barcode_type" optional="hide"/>
|
||||
<field name="barcode" force_save="1"/>
|
||||
<field name="price" attrs="{'invisible': [('barcode_type', 'not in', ('price', 'weight'))]}"/>
|
||||
<field name="quantity" attrs="{'invisible': [('barcode_type', '=', 'product')]}" optional="show"/>
|
||||
<field name="uom_id" attrs="{'invisible': [('barcode_type', '=', 'product')]}" optional="show"/>
|
||||
<field name="copies" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="generate" type="object" string="Generate Label" class="btn-primary" states="step1"/>
|
||||
<button name="generate" type="object" string="Generate Labels" class="btn-primary" states="step1"/>
|
||||
<button special="cancel" string="Cancel" class="btn-default" states="step1"/>
|
||||
<button name="print_zpl" type="object" string="Print" class="btn-primary" states="step2"/>
|
||||
<button name="print_zpl" type="object" string="Print and New" class="btn-primary" context="{'print_and_new': True}" attrs="{'invisible': ['|', ('state', '!=', 'step2'), ('barcode_type', '=', 'product')]}"/>
|
||||
<button special="cancel" string="Close" class="btn-default" states="step2"/>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
@@ -15,6 +15,8 @@ Product Priority Star
|
||||
|
||||
This module adds a priority star on products (like on pickings and manufacturing order). If the star is yellow, the product will be displayed at the top.
|
||||
|
||||
This feature is native in Odoo 16.0.
|
||||
|
||||
This module has been written by Alexis de Lattre from Akretion <alexis.delattre@akretion.com>.
|
||||
""",
|
||||
'author': 'Akretion',
|
||||
|
||||
@@ -37,6 +37,7 @@ This module has been written by Alexis de Lattre from Akretion <alexis.delattre@
|
||||
'views/product_product.xml',
|
||||
'views/product_config_menu.xml',
|
||||
'views/product_category_view.xml',
|
||||
'views/product_attribute_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ from . import product_template
|
||||
from . import product_supplierinfo
|
||||
from . import product_pricelist
|
||||
from . import product_category
|
||||
from . import product_attribute
|
||||
|
||||
27
product_usability/models/product_attribute.py
Normal file
27
product_usability/models/product_attribute.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) 2022 Akretion (<http://www.akretion.com>).
|
||||
# @author Kévin Roche <kevin.roche@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class ProductAttribute(models.Model):
|
||||
_inherit = "product.attribute"
|
||||
|
||||
values_count = fields.Integer(compute="_compute_values_count")
|
||||
|
||||
@api.depends("value_ids")
|
||||
def _compute_values_count(self):
|
||||
for attr in self:
|
||||
attr.values_count = len(attr.value_ids)
|
||||
|
||||
def show_values_ids(self):
|
||||
return {
|
||||
"name": "Attributes Lines",
|
||||
"type": "ir.actions.act_window",
|
||||
"res_id": self.id,
|
||||
"view_mode": "tree",
|
||||
"res_model": "product.attribute.value",
|
||||
"view_id":self.env.ref("product_usability.product_attribute_value_view_tree").id,
|
||||
"target": "current",
|
||||
"domain": [("id", "in", self.value_ids.ids)],
|
||||
}
|
||||
38
product_usability/views/product_attribute_view.xml
Normal file
38
product_usability/views/product_attribute_view.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="product_attribute_values_button" model="ir.ui.view">
|
||||
<field name="model">product.attribute</field>
|
||||
<field name="inherit_id" ref="product.product_attribute_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<group name="main_fields" position='before'>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="show_values_ids"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-tasks"
|
||||
>
|
||||
<field
|
||||
name="values_count"
|
||||
widget="statinfo"
|
||||
string="Attribute Values"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_attribute_value_view_tree">
|
||||
<field name="name">product.attribute.value.view.tree</field>
|
||||
<field name="model">product.attribute.value</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Attributes Values">
|
||||
<field name="name"/>
|
||||
<field name="is_custom"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -32,11 +32,13 @@
|
||||
<field name="model">product.pricelist.item</field>
|
||||
<field name="inherit_id" ref="product.product_pricelist_item_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="applied_on" position="before">
|
||||
<field name="pricelist_id" position="move"/>
|
||||
</field>
|
||||
<field name="pricelist_id" position="attributes">
|
||||
<attribute name="invisible">not context.get('product_pricelist_item_main_view')</attribute>
|
||||
<attribute name="invisible">context.get('from_product_pricelist_view')</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<field name="res_model">product.pricelist.item</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('pricelist_id', '=', active_id)]</field>
|
||||
<field name="context">{'product_pricelist_item_main_view': True}</field>
|
||||
<field name="context">{'product_pricelist_item_main_view': True, 'default_pricelist_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
<record id="product_pricelist_view" model="ir.ui.view">
|
||||
@@ -34,6 +34,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<field name="item_ids" position="attributes">
|
||||
<attribute name="context">{'from_product_pricelist_view': True, 'default_base': 'list_price'}</attribute>
|
||||
</field>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -32,5 +32,16 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_product_tree_view" model="ir.ui.view">
|
||||
<field name="name">usability.product.product.tree</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="product.product_product_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_id" position="before">
|
||||
<field name="seller_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<field name="inherit_id" ref="product.product_template_search_view" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="categ_id" position="after">
|
||||
<field name="seller_ids" string="Supplier" filter_domain="[('seller_ids.name', 'ilike', self)]"/>
|
||||
<field name="seller_id" string="Main Supplier"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
@@ -32,4 +32,14 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_tree_view" model="ir.ui.view">
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="barcode" position="after">
|
||||
<field name="seller_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
0
project_usability/__init__.py
Normal file
0
project_usability/__init__.py
Normal file
20
project_usability/__manifest__.py
Normal file
20
project_usability/__manifest__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2023 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': 'Project Usability',
|
||||
'version': '14.0.1.0.0',
|
||||
'category': 'Services/Project',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Usability improvements on project module',
|
||||
'author': 'Akretion',
|
||||
'website': 'https://github.com/akretion/odoo-usability',
|
||||
'depends': [
|
||||
'project',
|
||||
],
|
||||
'data': [
|
||||
'views/project_project.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
26
project_usability/views/project_project.xml
Normal file
26
project_usability/views/project_project.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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>
|
||||
|
||||
<!-- user_id is not displayed by default in project kanban view (it is native in v16) -->
|
||||
<record id="view_project_kanban" model="ir.ui.view">
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.view_project_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="user_id"/>
|
||||
</field>
|
||||
<xpath expr="//div[hasclass('o_project_kanban_boxes')]" position="inside">
|
||||
<a t-if="record.user_id.raw_value" class="o_project_kanban_box">
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
</a>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -23,7 +23,9 @@ Please contact Alexis de Lattre from Akretion <alexis.delattre@akretion.com> for
|
||||
'purchase_usability',
|
||||
],
|
||||
'data': [
|
||||
'views/purchase_order.xml',
|
||||
'views/stock_picking.xml',
|
||||
'views/stock_warehouse_orderpoint.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from . import purchase
|
||||
from . import stock_warehouse_orderpoint
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Copyright 2015-2020 Akretion (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockWarehouseOrderpoint(models.Model):
|
||||
_inherit = 'stock.warehouse.orderpoint'
|
||||
|
||||
# Field needed to be able to search on supplier in the "Replenish" tree view
|
||||
# I put it in purchase_stock_usability and not stock_usability
|
||||
# because I wanted to use the field 'show_supplier' defined in purchase_stock
|
||||
# (but I don't use it in the end because its computation returns False even
|
||||
# on products with a Buy route) and I may also
|
||||
# one day interact with supplier_id (M2O product.supplierinfo) defined in
|
||||
# purchase_stock
|
||||
seller_id = fields.Many2one(
|
||||
related='product_id.product_tmpl_id.seller_ids.name',
|
||||
store=True, string='Supplier',
|
||||
)
|
||||
43
purchase_stock_usability/views/purchase_order.xml
Normal file
43
purchase_stock_usability/views/purchase_order.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2023 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="purchase_order_tree" model="ir.ui.view">
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date_planned" position="after">
|
||||
<field name="picking_type_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="purchase_order_view_tree" model="ir.ui.view">
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date_planned" position="after">
|
||||
<field name="picking_type_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="purchase_order_kpis_tree" model="ir.ui.view">
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_kpis_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date_planned" position="after">
|
||||
<field name="picking_type_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2014-2020 Akretion (http://www.akretion.com/)
|
||||
@author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_warehouse_orderpoint_tree_editable" model="ir.ui.view">
|
||||
<field name="model">stock.warehouse.orderpoint</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_tree_editable" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="seller_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_warehouse_orderpoint_tree_editable_config" model="ir.ui.view">
|
||||
<field name="model">stock.warehouse.orderpoint</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_tree_editable_config" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="seller_id" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_reorder_report_search" model="ir.ui.view">
|
||||
<field name="model">stock.warehouse.orderpoint</field>
|
||||
<field name="inherit_id" ref="stock.stock_reorder_report_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="seller_id" domain="[('parent_id', '=', False)]"/>
|
||||
</field>
|
||||
<filter name="groupby_category" position="after">
|
||||
<filter string="Supplier" name="seller_id_groupby" context="{'group_by': 'seller_id'}"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -68,6 +68,13 @@ class PurchaseOrder(models.Model):
|
||||
# ]
|
||||
return res
|
||||
|
||||
def _prepare_invoice(self):
|
||||
# Don't write self.partner_ref on 'ref' of invoice... ref is for the
|
||||
# supplier invoice number !
|
||||
vals = super()._prepare_invoice()
|
||||
vals["ref"] = ''
|
||||
return vals
|
||||
|
||||
|
||||
class PurchaseOrderLine(models.Model):
|
||||
_inherit = 'purchase.order.line'
|
||||
|
||||
@@ -7,6 +7,7 @@ from odoo import api, models
|
||||
class CrmLead(models.Model):
|
||||
_inherit = 'crm.lead'
|
||||
|
||||
# not needed in v16 because it has been fixed in sale_crm
|
||||
def action_view_sale_quotation(self):
|
||||
action = super().action_view_sale_quotation()
|
||||
if 'search_default_partner_id' in action['context']:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright 2019 Akretion France (http://www.akretion.com)
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Sale Down Payment',
|
||||
'version': '12.0.1.0.0',
|
||||
'version': '14.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. There is also a button *Register Payment* on the sale order.
|
||||
After processing a bank statement, you can start a wizard to link unreconciled incoming payments to a sale order (NOT ported to v14 so far). There is also a button *Register Payment* on the sale order.
|
||||
|
||||
This module targets B2B companies that don't want to generate a down payment invoice for an advanced payment.
|
||||
|
||||
@@ -25,11 +25,13 @@ This module has been written by Alexis de Lattre from Akretion
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['sale'],
|
||||
'data': [
|
||||
'wizard/account_bank_statement_sale_view.xml',
|
||||
'views/account_bank_statement.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'wizard/account_payment_register_sale_view.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': False,
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from . import sale
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import account_payment
|
||||
|
||||
26
sale_down_payment/models/account_move.py
Normal file
26
sale_down_payment/models/account_move.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2023-2024 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _post(self, soft=True):
|
||||
res = super()._post(soft=soft)
|
||||
amlo = self.env['account.move.line']
|
||||
for move in self:
|
||||
if move.state == 'posted' and move.move_type == 'out_invoice':
|
||||
sales = move.invoice_line_ids.sale_line_ids.order_id
|
||||
if sales:
|
||||
mlines = amlo.search([('sale_id', 'in', sales.ids)])
|
||||
if mlines:
|
||||
mlines_to_reconcile = move.line_ids.filtered(
|
||||
lambda line: line.account_id ==
|
||||
move.commercial_partner_id.property_account_receivable_id)
|
||||
mlines_to_reconcile |= mlines
|
||||
mlines_to_reconcile.remove_move_reconcile()
|
||||
mlines_to_reconcile.reconcile()
|
||||
return res
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019 Akretion France (http://www.akretion.com)
|
||||
# Copyright 2019-2024 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
@@ -9,24 +9,23 @@ from odoo.exceptions import ValidationError
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
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')
|
||||
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)]")
|
||||
|
||||
@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_id.internal_type != 'receivable':
|
||||
if line.sale_id and line.account_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.name,
|
||||
line.sale_id.name,
|
||||
% (line.display_name,
|
||||
line.sale_id.display_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_id.user_type_id.type != 'receivable':
|
||||
if self.sale_id and self.account_internal_type != 'receivable':
|
||||
self.sale_id = False
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user