mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 21:12:02 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
12
odoo-bringout-oca-ocb-purchase/purchase/models/__init__.py
Normal file
12
odoo-bringout-oca-ocb-purchase/purchase/models/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_invoice
|
||||
from . import analytic_account
|
||||
from . import analytic_applicability
|
||||
from . import purchase
|
||||
from . import product
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
from . import mail_compose_message
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,286 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import logging
|
||||
import time
|
||||
|
||||
from odoo import api, fields, models, Command, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
TOLERANCE = 0.02 # tolerance applied to the total when searching for a matching purchase order
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
purchase_vendor_bill_id = fields.Many2one('purchase.bill.union', store=False, readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
string='Auto-complete',
|
||||
help="Auto-complete from a past bill / purchase order.")
|
||||
purchase_id = fields.Many2one('purchase.order', store=False, readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
string='Purchase Order',
|
||||
help="Auto-complete from a past purchase order.")
|
||||
purchase_order_count = fields.Integer(compute="_compute_origin_po_count", string='Purchase Order Count')
|
||||
|
||||
def _get_invoice_reference(self):
|
||||
self.ensure_one()
|
||||
vendor_refs = [ref for ref in set(self.invoice_line_ids.mapped('purchase_line_id.order_id.partner_ref')) if ref]
|
||||
if self.ref:
|
||||
return [ref for ref in self.ref.split(', ') if ref and ref not in vendor_refs] + vendor_refs
|
||||
return vendor_refs
|
||||
|
||||
@api.onchange('purchase_vendor_bill_id', 'purchase_id')
|
||||
def _onchange_purchase_auto_complete(self):
|
||||
r''' Load from either an old purchase order, either an old vendor bill.
|
||||
|
||||
When setting a 'purchase.bill.union' in 'purchase_vendor_bill_id':
|
||||
* If it's a vendor bill, 'invoice_vendor_bill_id' is set and the loading is done by '_onchange_invoice_vendor_bill'.
|
||||
* If it's a purchase order, 'purchase_id' is set and this method will load lines.
|
||||
|
||||
/!\ All this not-stored fields must be empty at the end of this function.
|
||||
'''
|
||||
if self.purchase_vendor_bill_id.vendor_bill_id:
|
||||
self.invoice_vendor_bill_id = self.purchase_vendor_bill_id.vendor_bill_id
|
||||
self._onchange_invoice_vendor_bill()
|
||||
elif self.purchase_vendor_bill_id.purchase_order_id:
|
||||
self.purchase_id = self.purchase_vendor_bill_id.purchase_order_id
|
||||
self.purchase_vendor_bill_id = False
|
||||
|
||||
if not self.purchase_id:
|
||||
return
|
||||
|
||||
# Copy data from PO
|
||||
invoice_vals = self.purchase_id.with_company(self.purchase_id.company_id)._prepare_invoice()
|
||||
has_invoice_lines = bool(self.invoice_line_ids.filtered(lambda x: x.display_type not in ('line_note', 'line_section')))
|
||||
new_currency_id = self.currency_id if has_invoice_lines else invoice_vals.get('currency_id')
|
||||
del invoice_vals['ref'], invoice_vals['payment_reference']
|
||||
del invoice_vals['company_id'] # avoid recomputing the currency
|
||||
if self.move_type == invoice_vals['move_type']:
|
||||
del invoice_vals['move_type'] # no need to be updated if it's same value, to avoid recomputes
|
||||
self.update(invoice_vals)
|
||||
self.currency_id = new_currency_id
|
||||
|
||||
# Copy purchase lines.
|
||||
po_lines = self.purchase_id.order_line - self.invoice_line_ids.mapped('purchase_line_id')
|
||||
for line in po_lines.filtered(lambda l: not l.display_type):
|
||||
self.invoice_line_ids += self.env['account.move.line'].new(
|
||||
line._prepare_account_move_line(self)
|
||||
)
|
||||
|
||||
# Compute invoice_origin.
|
||||
origins = set(self.invoice_line_ids.mapped('purchase_line_id.order_id.name'))
|
||||
self.invoice_origin = ','.join(list(origins))
|
||||
|
||||
# Compute ref.
|
||||
refs = self._get_invoice_reference()
|
||||
self.ref = ', '.join(refs)
|
||||
|
||||
# Compute payment_reference.
|
||||
if not self.payment_reference:
|
||||
if len(refs) == 1:
|
||||
self.payment_reference = refs[0]
|
||||
elif len(refs) > 1:
|
||||
self.payment_reference = refs[-1]
|
||||
|
||||
self.purchase_id = False
|
||||
|
||||
@api.onchange('partner_id', 'company_id')
|
||||
def _onchange_partner_id(self):
|
||||
res = super(AccountMove, self)._onchange_partner_id()
|
||||
|
||||
currency_id = (
|
||||
self.partner_id.property_purchase_currency_id
|
||||
or self.env['res.currency'].browse(self.env.context.get("default_currency_id"))
|
||||
or self.currency_id
|
||||
)
|
||||
|
||||
if self.partner_id and self.move_type in ['in_invoice', 'in_refund'] and self.currency_id != currency_id:
|
||||
if not self.env.context.get('default_journal_id'):
|
||||
journal_domain = [
|
||||
('type', '=', 'purchase'),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('currency_id', '=', currency_id.id),
|
||||
]
|
||||
default_journal_id = self.env['account.journal'].search(journal_domain, limit=1)
|
||||
if default_journal_id:
|
||||
self.journal_id = default_journal_id
|
||||
|
||||
self.currency_id = currency_id
|
||||
|
||||
return res
|
||||
|
||||
@api.depends('line_ids.purchase_line_id')
|
||||
def _compute_origin_po_count(self):
|
||||
for move in self:
|
||||
move.purchase_order_count = len(move.line_ids.purchase_line_id.order_id)
|
||||
|
||||
def action_view_source_purchase_orders(self):
|
||||
self.ensure_one()
|
||||
source_orders = self.line_ids.purchase_line_id.order_id
|
||||
result = self.env['ir.actions.act_window']._for_xml_id('purchase.purchase_form_action')
|
||||
if len(source_orders) > 1:
|
||||
result['domain'] = [('id', 'in', source_orders.ids)]
|
||||
elif len(source_orders) == 1:
|
||||
result['views'] = [(self.env.ref('purchase.purchase_order_form', False).id, 'form')]
|
||||
result['res_id'] = source_orders.id
|
||||
else:
|
||||
result = {'type': 'ir.actions.act_window_close'}
|
||||
return result
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# OVERRIDE
|
||||
moves = super(AccountMove, self).create(vals_list)
|
||||
for move in moves:
|
||||
if move.reversed_entry_id:
|
||||
continue
|
||||
purchases = move.line_ids.purchase_line_id.order_id
|
||||
if not purchases:
|
||||
continue
|
||||
refs = [purchase._get_html_link() for purchase in purchases]
|
||||
message = _("This vendor bill has been created from: %s") % ','.join(refs)
|
||||
move.message_post(body=message)
|
||||
return moves
|
||||
|
||||
def write(self, vals):
|
||||
# OVERRIDE
|
||||
old_purchases = [move.mapped('line_ids.purchase_line_id.order_id') for move in self]
|
||||
res = super(AccountMove, self).write(vals)
|
||||
for i, move in enumerate(self):
|
||||
new_purchases = move.mapped('line_ids.purchase_line_id.order_id')
|
||||
if not new_purchases:
|
||||
continue
|
||||
diff_purchases = new_purchases - old_purchases[i]
|
||||
if diff_purchases:
|
||||
refs = [purchase._get_html_link() for purchase in diff_purchases]
|
||||
message = _("This vendor bill has been modified from: %s") % ','.join(refs)
|
||||
move.message_post(body=message)
|
||||
return res
|
||||
|
||||
def find_matching_subset_invoice_lines(self, invoice_lines, goal_total, timeout):
|
||||
""" The problem of finding the subset of `invoice_lines` which sums up to `goal_total` reduces to the 0-1 Knapsack problem.
|
||||
The dynamic programming approach to solve this problem is most of the time slower than this because identical sub-problems don't arise often enough.
|
||||
It returns the list of invoice lines which sums up to `goal_total` or an empty list if multiple or no solutions were found."""
|
||||
def _find_matching_subset_invoice_lines(lines, goal):
|
||||
if time.time() - start_time > timeout:
|
||||
raise TimeoutError
|
||||
solutions = []
|
||||
for i, line in enumerate(lines):
|
||||
if line['amount_to_invoice'] < goal - TOLERANCE:
|
||||
sub_solutions = _find_matching_subset_invoice_lines(lines[i + 1:], goal - line['amount_to_invoice'])
|
||||
solutions.extend((line, *solution) for solution in sub_solutions)
|
||||
elif goal - TOLERANCE <= line['amount_to_invoice'] <= goal + TOLERANCE:
|
||||
solutions.append([line])
|
||||
if len(solutions) > 1:
|
||||
# More than 1 solution found, we can't know for sure which is the correct one, so we don't return any solution
|
||||
return []
|
||||
return solutions
|
||||
start_time = time.time()
|
||||
try:
|
||||
subsets = _find_matching_subset_invoice_lines(sorted(invoice_lines, key=lambda line: line['amount_to_invoice'], reverse=True), goal_total)
|
||||
return subsets[0] if subsets else []
|
||||
except TimeoutError:
|
||||
_logger.warning("Timed out during search of a matching subset of invoice lines")
|
||||
return []
|
||||
|
||||
def _set_purchase_orders(self, purchase_orders, force_write=True):
|
||||
with self.env.cr.savepoint():
|
||||
with self._get_edi_creation() as move_form:
|
||||
if force_write and move_form.line_ids:
|
||||
move_form.invoice_line_ids = [Command.clear()]
|
||||
for purchase_order in purchase_orders:
|
||||
move_form.invoice_line_ids = [Command.create({
|
||||
'display_type': 'line_section',
|
||||
'name': _('From %s document', purchase_order.name)
|
||||
})]
|
||||
move_form.purchase_id = purchase_order
|
||||
move_form._onchange_purchase_auto_complete()
|
||||
|
||||
def _match_purchase_orders(self, po_references, partner_id, amount_total, timeout):
|
||||
""" Tries to match a purchase order given some bill arguments/hints.
|
||||
|
||||
:param po_references: A list of potencial purchase order references/name.
|
||||
:param partner_id: The vendor id.
|
||||
:param amount_total: The vendor bill total.
|
||||
:param timeout: The timeout for subline search
|
||||
:return: A tuple containing:
|
||||
* a str which is the match method:
|
||||
'total_match': the invoice amount AND the partner or bill' reference match
|
||||
'subset_total_match': the reference AND a subset of line that match the bill amount total
|
||||
'po_match': only the reference match
|
||||
'no_match': no result found
|
||||
* recordset of matched 'purchase.order.line' (could come from more than one purchase.order)
|
||||
"""
|
||||
common_domain = [('company_id', '=', self.company_id.id), ('state', 'in', ('purchase', 'done')), ('invoice_status', 'in', ('to invoice', 'no'))]
|
||||
|
||||
matching_pos = self.env['purchase.order']
|
||||
if po_references and amount_total:
|
||||
matching_pos |= self.env['purchase.order'].search(common_domain + [('name', 'in', po_references)])
|
||||
|
||||
if not matching_pos:
|
||||
matching_pos |= self.env['purchase.order'].search(common_domain + [('partner_ref', 'in', po_references)])
|
||||
|
||||
if matching_pos:
|
||||
matching_pos_invoice_lines = [{
|
||||
'line': line,
|
||||
'amount_to_invoice': (1 - line.qty_invoiced / line.product_qty) * line.price_total,
|
||||
} for line in matching_pos.order_line if line.product_qty]
|
||||
|
||||
if amount_total - TOLERANCE < sum(line['amount_to_invoice'] for line in matching_pos_invoice_lines) < amount_total + TOLERANCE:
|
||||
return 'total_match', matching_pos.order_line
|
||||
|
||||
else:
|
||||
il_subset = self.find_matching_subset_invoice_lines(matching_pos_invoice_lines, amount_total, timeout)
|
||||
if il_subset:
|
||||
return 'subset_total_match', self.env['purchase.order.line'].union(*[line['line'] for line in il_subset])
|
||||
else:
|
||||
return 'po_match', matching_pos.order_line
|
||||
|
||||
if partner_id and amount_total:
|
||||
purchase_id_domain = common_domain + [('partner_id', 'child_of', [partner_id]), ('amount_total', '>=', amount_total - TOLERANCE), ('amount_total', '<=', amount_total + TOLERANCE)]
|
||||
matching_pos |= self.env['purchase.order'].search(purchase_id_domain)
|
||||
if len(matching_pos) == 1:
|
||||
return 'total_match', matching_pos.order_line
|
||||
|
||||
return 'no_match', matching_pos.order_line
|
||||
|
||||
def _find_and_set_purchase_orders(self, po_references, partner_id, amount_total, prefer_purchase_line=False, timeout=10):
|
||||
self.ensure_one()
|
||||
|
||||
method, matched_po_lines = self._match_purchase_orders(po_references, partner_id, amount_total, timeout)
|
||||
|
||||
if method == 'total_match': # erase all lines and autocomplete
|
||||
self._set_purchase_orders(matched_po_lines.order_id, force_write=True)
|
||||
|
||||
elif method == 'subset_total_match': # don't erase and add autocomplete
|
||||
self._set_purchase_orders(matched_po_lines.order_id, force_write=False)
|
||||
|
||||
with self._get_edi_creation() as move_form: # logic for unmatched lines
|
||||
unmatched_lines = move_form.invoice_line_ids.filtered(
|
||||
lambda l: l.purchase_line_id and l.purchase_line_id not in matched_po_lines)
|
||||
for line in unmatched_lines:
|
||||
if prefer_purchase_line:
|
||||
line.quantity = 0
|
||||
else:
|
||||
line.unlink()
|
||||
|
||||
if not prefer_purchase_line:
|
||||
move_form.invoice_line_ids.filtered('purchase_line_id').quantity = 0
|
||||
|
||||
elif method == 'po_match': # erase all lines and autocomplete
|
||||
if prefer_purchase_line:
|
||||
self._set_purchase_orders(matched_po_lines.order_id, force_write=True)
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
""" Override AccountInvoice_line to add the link to the purchase order line it is related to"""
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', index='btree_not_null')
|
||||
purchase_order_id = fields.Many2one('purchase.order', 'Purchase Order', related='purchase_line_id.order_id', readonly=True)
|
||||
|
||||
def _copy_data_extend_business_fields(self, values):
|
||||
# OVERRIDE to copy the 'purchase_line_id' field as well.
|
||||
super(AccountMoveLine, self)._copy_data_extend_business_fields(values)
|
||||
values['purchase_line_id'] = self.purchase_line_id.id
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class AccountAnalyticAccount(models.Model):
|
||||
_inherit = 'account.analytic.account'
|
||||
|
||||
purchase_order_count = fields.Integer("Purchase Order Count", compute='_compute_purchase_order_count')
|
||||
|
||||
@api.depends('line_ids')
|
||||
def _compute_purchase_order_count(self):
|
||||
for account in self:
|
||||
account.purchase_order_count = self.env['purchase.order'].search_count([
|
||||
('order_line.invoice_lines.analytic_line_ids.account_id', '=', account.id)
|
||||
])
|
||||
|
||||
def action_view_purchase_orders(self):
|
||||
self.ensure_one()
|
||||
purchase_orders = self.env['purchase.order'].search([
|
||||
('order_line.invoice_lines.analytic_line_ids.account_id', '=', self.id)
|
||||
])
|
||||
result = {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "purchase.order",
|
||||
"domain": [['id', 'in', purchase_orders.ids]],
|
||||
"name": _("Purchase Orders"),
|
||||
'view_mode': 'tree,form',
|
||||
}
|
||||
if len(purchase_orders) == 1:
|
||||
result['view_mode'] = 'form'
|
||||
result['res_id'] = purchase_orders.id
|
||||
return result
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountAnalyticApplicability(models.Model):
|
||||
_inherit = 'account.analytic.applicability'
|
||||
_description = "Analytic Plan's Applicabilities"
|
||||
|
||||
business_domain = fields.Selection(
|
||||
selection_add=[
|
||||
('purchase_order', 'Purchase Order'),
|
||||
],
|
||||
ondelete={'purchase_order': 'cascade'},
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# purches Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailComposeMessage(models.TransientModel):
|
||||
_inherit = 'mail.compose.message'
|
||||
|
||||
def _action_send_mail(self, auto_commit=False):
|
||||
if self.model == 'purchase.order':
|
||||
self = self.with_context(mailing_document_based=True)
|
||||
if self.env.context.get('mark_rfq_as_sent'):
|
||||
self = self.with_context(mail_notify_author=self.env.user.partner_id in self.partner_ids)
|
||||
return super(MailComposeMessage, self)._action_send_mail(auto_commit=auto_commit)
|
||||
99
odoo-bringout-oca-ocb-purchase/purchase/models/product.py
Normal file
99
odoo-bringout-oca-ocb-purchase/purchase/models/product.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
|
||||
from odoo.tools.float_utils import float_round
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_name = 'product.template'
|
||||
_inherit = 'product.template'
|
||||
|
||||
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased', digits='Product Unit of Measure')
|
||||
purchase_method = fields.Selection([
|
||||
('purchase', 'On ordered quantities'),
|
||||
('receive', 'On received quantities'),
|
||||
], string="Control Policy", compute='_compute_purchase_method', default='receive', store=True, readonly=False,
|
||||
help="On ordered quantities: Control bills based on ordered quantities.\n"
|
||||
"On received quantities: Control bills based on received quantities.")
|
||||
purchase_line_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Line Warning', help=WARNING_HELP, required=True, default="no-message")
|
||||
purchase_line_warn_msg = fields.Text('Message for Purchase Order Line')
|
||||
|
||||
@api.depends('detailed_type')
|
||||
def _compute_purchase_method(self):
|
||||
default_purchase_method = self.env['product.template'].default_get(['purchase_method']).get('purchase_method')
|
||||
for product in self:
|
||||
if product.detailed_type == 'service':
|
||||
product.purchase_method = 'purchase'
|
||||
else:
|
||||
product.purchase_method = default_purchase_method
|
||||
|
||||
def _compute_purchased_product_qty(self):
|
||||
for template in self.with_context(active_test=False):
|
||||
template.purchased_product_qty = float_round(sum(p.purchased_product_qty for
|
||||
p in template.product_variant_ids), precision_rounding=template.uom_id.rounding
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_import_templates(self):
|
||||
res = super(ProductTemplate, self).get_import_templates()
|
||||
if self.env.context.get('purchase_product_template'):
|
||||
return [{
|
||||
'label': _('Import Template for Products'),
|
||||
'template': '/purchase/static/xls/product_purchase.xls'
|
||||
}]
|
||||
return res
|
||||
|
||||
def action_view_po(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("purchase.action_purchase_history")
|
||||
action['domain'] = [
|
||||
('state', 'in', ['purchase', 'done']),
|
||||
('product_id', 'in', self.with_context(active_test=False).product_variant_ids.ids),
|
||||
]
|
||||
action['display_name'] = _("Purchase History for %s", self.display_name)
|
||||
return action
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_name = 'product.product'
|
||||
_inherit = 'product.product'
|
||||
|
||||
purchased_product_qty = fields.Float(compute='_compute_purchased_product_qty', string='Purchased',
|
||||
digits='Product Unit of Measure')
|
||||
|
||||
def _compute_purchased_product_qty(self):
|
||||
date_from = fields.Datetime.to_string(fields.Date.context_today(self) - relativedelta(years=1))
|
||||
domain = [
|
||||
('order_id.state', 'in', ['purchase', 'done']),
|
||||
('product_id', 'in', self.ids),
|
||||
('order_id.date_approve', '>=', date_from)
|
||||
]
|
||||
order_lines = self.env['purchase.order.line']._read_group(domain, ['product_id', 'product_uom_qty'], ['product_id'])
|
||||
purchased_data = dict([(data['product_id'][0], data['product_uom_qty']) for data in order_lines])
|
||||
for product in self:
|
||||
if not product.id:
|
||||
product.purchased_product_qty = 0.0
|
||||
continue
|
||||
product.purchased_product_qty = float_round(purchased_data.get(product.id, 0), precision_rounding=product.uom_id.rounding)
|
||||
|
||||
def action_view_po(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("purchase.action_purchase_history")
|
||||
action['domain'] = ['&', ('state', 'in', ['purchase', 'done']), ('product_id', 'in', self.ids)]
|
||||
action['display_name'] = _("Purchase History for %s", self.display_name)
|
||||
return action
|
||||
|
||||
|
||||
class ProductSupplierinfo(models.Model):
|
||||
_inherit = "product.supplierinfo"
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.company.currency_id.id
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
|
||||
purchase = fields.Boolean("Purchase", default=True, help="If true, the packaging can be used for purchase orders")
|
||||
1512
odoo-bringout-oca-ocb-purchase/purchase/models/purchase.py
Normal file
1512
odoo-bringout-oca-ocb-purchase/purchase/models/purchase.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
po_lead = fields.Float(string='Purchase Lead Time', required=True,
|
||||
help="Margin of error for vendor lead times. When the system "
|
||||
"generates Purchase Orders for procuring products, "
|
||||
"they will be scheduled that many days earlier "
|
||||
"to cope with unexpected vendor delays.", default=0.0)
|
||||
|
||||
po_lock = fields.Selection([
|
||||
('edit', 'Allow to edit purchase orders'),
|
||||
('lock', 'Confirmed purchase orders are not editable')
|
||||
], string="Purchase Order Modification", default="edit",
|
||||
help='Purchase Order Modification used when you want to purchase order editable after confirm')
|
||||
|
||||
po_double_validation = fields.Selection([
|
||||
('one_step', 'Confirm purchase orders in one step'),
|
||||
('two_step', 'Get 2 levels of approvals to confirm a purchase order')
|
||||
], string="Levels of Approvals", default='one_step',
|
||||
help="Provide a double validation mechanism for purchases")
|
||||
|
||||
po_double_validation_amount = fields.Monetary(string='Double validation amount', default=5000,
|
||||
help="Minimum amount for which a double validation is required")
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
lock_confirmed_po = fields.Boolean("Lock Confirmed Orders", default=lambda self: self.env.company.po_lock == 'lock')
|
||||
po_lock = fields.Selection(related='company_id.po_lock', string="Purchase Order Modification *", readonly=False)
|
||||
po_order_approval = fields.Boolean("Purchase Order Approval", default=lambda self: self.env.company.po_double_validation == 'two_step')
|
||||
po_double_validation = fields.Selection(related='company_id.po_double_validation', string="Levels of Approvals *", readonly=False)
|
||||
po_double_validation_amount = fields.Monetary(related='company_id.po_double_validation_amount', string="Minimum Amount", currency_field='company_currency_id', readonly=False)
|
||||
company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', string="Company Currency", readonly=True)
|
||||
default_purchase_method = fields.Selection([
|
||||
('purchase', 'Ordered quantities'),
|
||||
('receive', 'Received quantities'),
|
||||
], string="Bill Control", default_model="product.template",
|
||||
help="This default value is applied to any new product created. "
|
||||
"This can be changed in the product detail form.", default="receive")
|
||||
group_warning_purchase = fields.Boolean("Purchase Warnings", implied_group='purchase.group_warning_purchase')
|
||||
module_account_3way_match = fields.Boolean("3-way matching: purchases, receptions and bills")
|
||||
module_purchase_requisition = fields.Boolean("Purchase Agreements")
|
||||
module_purchase_product_matrix = fields.Boolean("Purchase Grid Entry")
|
||||
po_lead = fields.Float(related='company_id.po_lead', readonly=False)
|
||||
use_po_lead = fields.Boolean(
|
||||
string="Security Lead Time for Purchase",
|
||||
config_parameter='purchase.use_po_lead',
|
||||
help="Margin of error for vendor lead times. When the system generates Purchase Orders for reordering products,they will be scheduled that many days earlier to cope with unexpected vendor delays.")
|
||||
|
||||
group_send_reminder = fields.Boolean("Receipt Reminder", implied_group='purchase.group_send_reminder', default=True,
|
||||
help="Allow automatically send email to remind your vendor the receipt date")
|
||||
|
||||
@api.onchange('use_po_lead')
|
||||
def _onchange_use_po_lead(self):
|
||||
if not self.use_po_lead:
|
||||
self.po_lead = 0.0
|
||||
|
||||
@api.onchange('group_product_variant')
|
||||
def _onchange_group_product_variant_purchase(self):
|
||||
"""If the user disables the product variants -> disable the product configurator as well"""
|
||||
if self.module_purchase_product_matrix and not self.group_product_variant:
|
||||
self.module_purchase_product_matrix = False
|
||||
|
||||
@api.onchange('module_purchase_product_matrix')
|
||||
def _onchange_module_purchase_product_matrix(self):
|
||||
"""The product variant grid requires the product variants activated
|
||||
If the user enables the product configurator -> enable the product variants as well"""
|
||||
if self.module_purchase_product_matrix and not self.group_product_variant:
|
||||
self.group_product_variant = True
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
po_lock = 'lock' if self.lock_confirmed_po else 'edit'
|
||||
po_double_validation = 'two_step' if self.po_order_approval else 'one_step'
|
||||
if self.po_lock != po_lock:
|
||||
self.po_lock = po_lock
|
||||
if self.po_double_validation != po_double_validation:
|
||||
self.po_double_validation = po_double_validation
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
|
||||
|
||||
|
||||
class res_partner(models.Model):
|
||||
_name = 'res.partner'
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def _compute_purchase_order_count(self):
|
||||
# retrieve all children partners
|
||||
all_partners = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
|
||||
purchase_order_groups = self.env['purchase.order']._read_group(
|
||||
domain=[('partner_id', 'in', all_partners.ids)],
|
||||
fields=['partner_id'], groupby=['partner_id']
|
||||
)
|
||||
partners = self.browse()
|
||||
for group in purchase_order_groups:
|
||||
partner = self.browse(group['partner_id'][0])
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.purchase_order_count += group['partner_id_count']
|
||||
partners |= partner
|
||||
partner = partner.parent_id
|
||||
(self - partners).purchase_order_count = 0
|
||||
|
||||
@api.model
|
||||
def _commercial_fields(self):
|
||||
return super(res_partner, self)._commercial_fields()
|
||||
|
||||
property_purchase_currency_id = fields.Many2one(
|
||||
'res.currency', string="Supplier Currency", company_dependent=True,
|
||||
help="This currency will be used, instead of the default one, for purchases from the current partner")
|
||||
purchase_order_count = fields.Integer(compute='_compute_purchase_order_count', string='Purchase Order Count')
|
||||
purchase_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Warning', help=WARNING_HELP, default="no-message")
|
||||
purchase_warn_msg = fields.Text('Message for Purchase Order')
|
||||
|
||||
receipt_reminder_email = fields.Boolean('Receipt Reminder', default=False, company_dependent=True,
|
||||
help="Automatically send a confirmation email to the vendor X days before the expected receipt date, asking him to confirm the exact date.")
|
||||
reminder_date_before_receipt = fields.Integer('Days Before Receipt', default=1, company_dependent=True,
|
||||
help="Number of days to send reminder email before the promised receipt date")
|
||||
Loading…
Add table
Add a link
Reference in a new issue