[IMP] extract function for docx report printed from docx template
This commit is contained in:
parent
20e7dc02de
commit
38d49cac61
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
*.pyc
|
||||
idea/
|
||||
.DS_store
|
||||
|
10
README.md
10
README.md
@ -1,9 +1 @@
|
||||
# Account Contracts
|
||||
|
||||
## Summary
|
||||
Create and storage form for contracts. Generate a print form of document of contract.
|
||||
|
||||
## Features
|
||||
- Contract management: create, sign, close, renew
|
||||
- Create .docx form of contract using any document template
|
||||
- Fields in document fill from Odoo models
|
||||
# DOCS report
|
||||
|
11
TODOLIST.md
11
TODOLIST.md
@ -1,11 +0,0 @@
|
||||
# TODO LIST
|
||||
|
||||
## Features
|
||||
|
||||
## Fixes
|
||||
- Change all `parents` to `genitive`
|
||||
- Merge `document_type` and `template_type` in `res.partner.document.template`
|
||||
- Change `annex_number` to `annex_counter`
|
||||
|
||||
## Big feature
|
||||
- Separate XML actions that generates transient fields for all types of documents
|
@ -1,2 +1 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
@ -1,28 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Client Contracts",
|
||||
"name": "DOCX report",
|
||||
"summary": """
|
||||
Print forms for contracts with clients""",
|
||||
"description": """
|
||||
Module for storing and creating print forms for contracts.
|
||||
""",
|
||||
Print docx report from docx template""",
|
||||
"description": """""",
|
||||
"author": "RYDLAB",
|
||||
"website": "http://rydlab.ru",
|
||||
"category": "Invoicing & Payments",
|
||||
"version": "14.0.1.0.0",
|
||||
"depends": ["base", "contacts", "hr", "l10n_ru", "sale", "sale_margin"],
|
||||
"category": "Technical",
|
||||
"version": "0.0.1",
|
||||
"depends": ["base"],
|
||||
"external_dependencies": {"python": ["docxtpl", "num2words"]},
|
||||
"data": [
|
||||
"data/assets_extension.xml",
|
||||
"data/fields_default.xml",
|
||||
"data/payment_terms.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/res_partner_contract.xml",
|
||||
"views/res_partner_contract_annex.xml",
|
||||
"views/res_partner_contract_field.xml",
|
||||
"views/res_partner_document_template.xml",
|
||||
"views/res_partner.xml",
|
||||
"views/sale_order.xml",
|
||||
"wizard/res_partner_contract_wizard.xml",
|
||||
"views/ir_actions_report_views.xml",
|
||||
],
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,27 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="payment_term_prepaid" model="account.payment.term">
|
||||
<field name="name">100% Prepaid</field>
|
||||
<field name="note">Payment terms: 100% Prepaid</field>
|
||||
<field name="sequence">1</field>
|
||||
</record>
|
||||
<record id="payment_term_postpayment" model="account.payment.term">
|
||||
<field name="name">100% Postpaid</field>
|
||||
<field name="note">Payment terms: 100% Postpaid</field>
|
||||
<field name="sequence">2</field>
|
||||
</record>
|
||||
<record id="payment_term_partial_2" model="account.payment.term">
|
||||
<field name="name">2 stages</field>
|
||||
<field name="note">Payment terms: Partial 2 Stages</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
<record id="payment_term_partial_3" model="account.payment.term">
|
||||
<field name="name">3 stages</field>
|
||||
<field name="note">Payment terms: Partial 3 Stages</field>
|
||||
<field name="sequence">4</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
File diff suppressed because it is too large
Load Diff
1140
i18n/ru.po
1140
i18n/ru.po
File diff suppressed because it is too large
Load Diff
@ -1,9 +1 @@
|
||||
from . import account_invoice
|
||||
from . import product_product
|
||||
from . import res_partner
|
||||
from . import res_partner_contract
|
||||
from . import res_partner_contract_annex
|
||||
from . import res_partner_contract_field
|
||||
from . import res_partner_contract_field_transient
|
||||
from . import res_partner_document_template
|
||||
from . import sale_order
|
||||
from . import ir_actions_report
|
||||
|
@ -1,67 +0,0 @@
|
||||
from odoo import models, _
|
||||
from odoo.exceptions import UserError
|
||||
from ..utils import MODULE_NAME
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
@staticmethod
|
||||
def check_contract_presence(sale_order_ids):
|
||||
error_message = ""
|
||||
if any(not so.contract_annex_id.contract_id for so in sale_order_ids):
|
||||
error_message = _("There is a Sale order without binding contract.")
|
||||
if any(not so.contract_annex_id for so in sale_order_ids):
|
||||
error_message = _("There is a Sale order without annex.")
|
||||
if error_message:
|
||||
raise UserError(error_message)
|
||||
|
||||
def action_invoice_print(self):
|
||||
"""
|
||||
for so in self.env["sale.order"].search([]):
|
||||
if self.id in so.invoice_ids.ids:
|
||||
order = so
|
||||
break
|
||||
else:
|
||||
return super().action_invoice_print()
|
||||
|
||||
if not order.contract_annex_id or not order.contract_annex_id.contract_id:
|
||||
raise UserError(
|
||||
_(
|
||||
"There is no binding contract. It is necessary to link the order with the annex to the contract."
|
||||
)
|
||||
)
|
||||
self.sent = True
|
||||
"""
|
||||
|
||||
sale_orders_ids = self.env["sale.order"].search(
|
||||
[("invoice_ids", "in", self.ids)]
|
||||
)
|
||||
if not sale_orders_ids:
|
||||
return super().action_invoice_print()
|
||||
|
||||
self.check_contract_presence(sale_orders_ids)
|
||||
self.filtered(lambda inv: not inv.is_move_sent).write({"is_move_sent": True})
|
||||
|
||||
view = self.env.ref(
|
||||
"{}.res_partner_wizard_print_document_view".format(MODULE_NAME)
|
||||
)
|
||||
# annex = order.contract_annex_id
|
||||
return {
|
||||
"name": _("Print Form of Contract Annex"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "res.partner.contract.wizard",
|
||||
"view_mode": "form",
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"context": {
|
||||
# "self_id": annex.id,
|
||||
"active_ids": self.ids,
|
||||
"active_model": "res.partner.contract.annex",
|
||||
# "company_form": annex.partner_id.company_form
|
||||
# if annex.partner_id.is_company
|
||||
# else "person",
|
||||
"attachment_model": self._name,
|
||||
"attachment_res_id": self.id,
|
||||
},
|
||||
}
|
183
models/ir_actions_report.py
Normal file
183
models/ir_actions_report.py
Normal file
@ -0,0 +1,183 @@
|
||||
import io
|
||||
from collections import OrderedDict
|
||||
from jinja2 import Environment as Jinja2Environment
|
||||
from logging import getLogger
|
||||
|
||||
from docxcompose.composer import Composer
|
||||
from docx import Document
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
from odoo import _, api, fields, models, SUPERUSER_ID
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.sql_db import TestCursor
|
||||
from odoo.tools.safe_eval import safe_eval, time
|
||||
|
||||
from ..utils.num2words import num2words_, num2words_currency
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = "ir.actions.actions"
|
||||
|
||||
report_name = fields.Char(required=False)
|
||||
report_type = fields.Selection(
|
||||
selection_add=[("docx-docx", "DOCX")], ondelete="cascade"
|
||||
)
|
||||
report_docx_template = fields.Binary(
|
||||
string="Report docx template",
|
||||
)
|
||||
|
||||
def _render_docx_docx(self, res_ids=None, data=None):
|
||||
if not data:
|
||||
data = {}
|
||||
data.setdefault("report_type", "docx")
|
||||
|
||||
# access the report details with sudo() but evaluation context as current user
|
||||
self_sudo = self.sudo()
|
||||
|
||||
save_in_attachment = OrderedDict()
|
||||
# Maps the streams in `save_in_attachment` back to the records they came from
|
||||
stream_record = dict()
|
||||
if res_ids:
|
||||
Model = self.env[self_sudo.model]
|
||||
record_ids = Model.browse(res_ids)
|
||||
docx_record_ids = Model
|
||||
if self_sudo.attachment:
|
||||
for record_id in record_ids:
|
||||
attachment = self_sudo.retrieve_attachment(record_id)
|
||||
if attachment:
|
||||
stream = self_sudo._retrieve_stream_from_attachment(attachment)
|
||||
save_in_attachment[record_id.id] = stream
|
||||
stream_record[stream] = record_id
|
||||
if not self_sudo.attachment_use or not attachment:
|
||||
docx_record_ids += record_id
|
||||
else:
|
||||
docx_record_ids = record_ids
|
||||
res_ids = docx_record_ids.ids
|
||||
|
||||
if save_in_attachment and not res_ids:
|
||||
_logger.info("The DOCS report has been generated from attachments.")
|
||||
return self_sudo._post_docx(save_in_attachment), "docx"
|
||||
|
||||
template = self.report_docx_template
|
||||
template_path = template._full_path(template.store_fname)
|
||||
|
||||
doc = DocxTemplate(template_path)
|
||||
|
||||
jinja_env = Jinja2Environment()
|
||||
|
||||
functions = {
|
||||
"number2words": num2words_,
|
||||
"currency2words": num2words_currency,
|
||||
}
|
||||
jinja_env.globals.update(**functions)
|
||||
|
||||
doc.render(data, jinja_env)
|
||||
|
||||
docx_content = io.BytesIO()
|
||||
doc.save(docx_content)
|
||||
docx_content.seek(0)
|
||||
|
||||
if res_ids:
|
||||
_logger.info(
|
||||
"The DOCS report has been generated for model: %s, records %s."
|
||||
% (self_sudo.model, str(res_ids))
|
||||
)
|
||||
return (
|
||||
self_sudo._post_docx(
|
||||
save_in_attachment, docx_content=docx_content, res_ids=res_ids
|
||||
),
|
||||
"docx",
|
||||
)
|
||||
return docx_content, "docx"
|
||||
|
||||
def _post_docx(self, save_in_attachment, docx_content=None, res_ids=None):
|
||||
def close_streams(streams):
|
||||
for stream in streams:
|
||||
try:
|
||||
stream.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(save_in_attachment) == 1 and not docx_content:
|
||||
return list(save_in_attachment.values())[0].getvalue()
|
||||
|
||||
streams = []
|
||||
|
||||
if docx_content:
|
||||
# Build a record_map mapping id -> record
|
||||
record_map = {
|
||||
r.id: r
|
||||
for r in self.env[self.model].browse(
|
||||
[res_id for res_id in res_ids if res_id]
|
||||
)
|
||||
}
|
||||
|
||||
# If no value in attachment or no record specified, only append the whole docx.
|
||||
if not record_map or not self.attachment:
|
||||
streams.append(docx_content)
|
||||
else:
|
||||
if len(res_ids) == 1:
|
||||
# Only one record, so postprocess directly and append the whole docx.
|
||||
if (
|
||||
res_ids[0] in record_map
|
||||
and not res_ids[0] in save_in_attachment
|
||||
):
|
||||
new_stream = self._postprocess_docx_report(
|
||||
record_map[res_ids[0]], docx_content
|
||||
)
|
||||
# If the buffer has been modified, mark the old buffer to be closed as well.
|
||||
if new_stream and new_stream != docx_content:
|
||||
close_streams([docx_content])
|
||||
docx_content = new_stream
|
||||
streams.append(docx_content)
|
||||
else:
|
||||
streams.append(docx_content)
|
||||
|
||||
if self.attachment_use:
|
||||
for stream in save_in_attachment.values():
|
||||
streams.append(stream)
|
||||
|
||||
if len(streams) == 1:
|
||||
result = streams[0].getvalue()
|
||||
else:
|
||||
try:
|
||||
result = self._merge_docx(streams)
|
||||
except Exception:
|
||||
raise UserError(_("One of the documents, you try to merge is fallback"))
|
||||
|
||||
close_streams(streams)
|
||||
return result
|
||||
|
||||
def _postprocess_docx_report(self, record, buffer):
|
||||
attachment_name = safe_eval(self.attachment, {"object": record, "time": time})
|
||||
if not attachment_name:
|
||||
return None
|
||||
attachment_vals = {
|
||||
"name": attachment_name,
|
||||
"raw": buffer.getvalue(),
|
||||
"res_model": self.model,
|
||||
"res_id": record.id,
|
||||
"type": "binary",
|
||||
}
|
||||
try:
|
||||
self.env["ir.attachment"].create(attachment_vals)
|
||||
except AccessError:
|
||||
_logger.info(
|
||||
"Cannot save DOCX report %r as attachment", attachment_vals["name"]
|
||||
)
|
||||
else:
|
||||
_logger.info(
|
||||
"The DOCX document %s is now saved in the database",
|
||||
attachment_vals["name"],
|
||||
)
|
||||
return buffer
|
||||
|
||||
def _merge_docx(self, streams):
|
||||
writer = Document()
|
||||
composer = Composer(writer)
|
||||
for stream in streams:
|
||||
reader = Document(stream)
|
||||
composer.append(reader)
|
||||
return composer.getvalue()
|
@ -1,12 +0,0 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
description_sale = fields.Text(
|
||||
"Sale Description",
|
||||
translate=True,
|
||||
help="A product's description you want to tell to your customers.\n"
|
||||
"This description will be copied to every Sales Order, Delivery Order and Customer Invoice/Credit Note",
|
||||
)
|
@ -1,73 +0,0 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
name_write = fields.Char(
|
||||
string="Name in contracts",
|
||||
help="This name used in contracts",
|
||||
)
|
||||
name_genitive = fields.Char(
|
||||
string="Name Genitive",
|
||||
)
|
||||
name_initials = fields.Char(
|
||||
string="Name Initials",
|
||||
)
|
||||
function_genitive = fields.Char(
|
||||
string="Job position genitive",
|
||||
)
|
||||
client_contract_ids = fields.One2many(
|
||||
"res.partner.contract",
|
||||
"partner_id",
|
||||
string="Contracts",
|
||||
)
|
||||
contract_count = fields.Integer(
|
||||
compute="_compute_contract_count", string="# of contracts"
|
||||
)
|
||||
full_address = fields.Char(
|
||||
compute="_compute_full_address"
|
||||
) # Check for res.partner.contact_address in base/res
|
||||
street_actual = fields.Many2one(
|
||||
"res.partner",
|
||||
string="Actual Address",
|
||||
)
|
||||
representative_id = fields.Many2one(
|
||||
"res.partner", string="Representative", help="Person representing company"
|
||||
)
|
||||
representative_document = fields.Char(
|
||||
string="Representative acts on the basis of",
|
||||
help="Parent Case",
|
||||
)
|
||||
signature = fields.Binary(string="Client signature")
|
||||
whatsapp = fields.Char(
|
||||
string="WhatsApp",
|
||||
help="If a contact have a WhatsApp number",
|
||||
)
|
||||
telegram = fields.Char(
|
||||
string="Telegram",
|
||||
help="If a contact have a Telegram number or identifier",
|
||||
)
|
||||
|
||||
@api.depends("street", "street2", "city", "state_id", "zip", "country_id")
|
||||
def _compute_full_address(self):
|
||||
for record in self:
|
||||
address_data = filter(
|
||||
None,
|
||||
map(
|
||||
lambda s: s and s.strip(),
|
||||
[
|
||||
record.zip,
|
||||
record.street,
|
||||
record.street2,
|
||||
record.city,
|
||||
record.country_id.l10n_ru_short_name or record.country_id.name,
|
||||
],
|
||||
),
|
||||
)
|
||||
record.full_address = ", ".join(address_data)
|
||||
|
||||
@api.depends("self.client_contract_ids")
|
||||
def _compute_contract_count(self):
|
||||
self.ensure_one()
|
||||
self.contract_count = len(self.client_contract_ids)
|
@ -1,157 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
from ..utils import MODULE_NAME
|
||||
|
||||
# from ..utils.misc import Extension, IDocument
|
||||
|
||||
|
||||
class PartnerContract(models.Model): # , IDocument, Extension):
|
||||
_name = "res.partner.contract"
|
||||
_description = "Contract"
|
||||
_inherit = [
|
||||
"mail.thread",
|
||||
"mail.activity.mixin",
|
||||
"mail.followers",
|
||||
"client_contracts.utils",
|
||||
]
|
||||
|
||||
def _get_default_name(self):
|
||||
"""Returns name format `№YYMM-D-N`,
|
||||
where N is a sequence number of contracts which are created this day
|
||||
"""
|
||||
current_day_ts = (
|
||||
datetime.datetime.now()
|
||||
.replace(minute=0, hour=0, second=0, microsecond=0)
|
||||
.timestamp()
|
||||
)
|
||||
|
||||
contracts_today = self.search([("create_date_ts", ">=", current_day_ts)])
|
||||
|
||||
contract_date = "{format_date}-{number}".format(
|
||||
format_date=datetime.date.strftime(datetime.date.today(), "%y%m-%d"),
|
||||
number=len(contracts_today) + 1,
|
||||
)
|
||||
|
||||
return contract_date
|
||||
|
||||
@staticmethod
|
||||
def _get_default_create_date_ts():
|
||||
"""Returns timestamp of now by local datetime"""
|
||||
return datetime.datetime.now().timestamp()
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
"res.partner",
|
||||
string="Partner",
|
||||
default=lambda self: self.env.context.get("active_id"),
|
||||
required=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
"res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
create_date_ts = fields.Char(default=_get_default_create_date_ts)
|
||||
res_model = fields.Char(default=lambda self: self._name)
|
||||
name = fields.Char(
|
||||
string="Contract number",
|
||||
default=_get_default_name,
|
||||
)
|
||||
create_date = fields.Datetime(string="Created on")
|
||||
date_conclusion = fields.Date(
|
||||
string="Signing date in system",
|
||||
)
|
||||
date_conclusion_fix = fields.Date(
|
||||
string="Actual signing date",
|
||||
help="Field for pointing out manually when contract is signed or closed",
|
||||
default=lambda self: self.date_conclusion,
|
||||
)
|
||||
contract_annex_ids = fields.One2many(
|
||||
comodel_name="res.partner.contract.annex",
|
||||
inverse_name="contract_id",
|
||||
string="Annexes",
|
||||
help="Annexes to this contract",
|
||||
)
|
||||
contract_annex_number = fields.Integer(
|
||||
default=1, help="Counter for generate Annex name"
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "New"),
|
||||
("sign", "Signed"),
|
||||
("close", "Closed"),
|
||||
],
|
||||
string="Status",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
index=True,
|
||||
tracking=True,
|
||||
track_sequence=3,
|
||||
default="draft",
|
||||
)
|
||||
|
||||
def action_sign(self):
|
||||
self.write({"state": "sign", "date_conclusion": fields.Date.today()})
|
||||
|
||||
def action_close(self):
|
||||
self.write({"state": "close"})
|
||||
|
||||
def action_renew(self):
|
||||
self.write({"state": "draft"})
|
||||
|
||||
def action_print_form(self):
|
||||
self.ensure_one()
|
||||
view = self.env.ref(
|
||||
"{}.res_partner_wizard_print_document_view".format(MODULE_NAME)
|
||||
)
|
||||
return {
|
||||
"name": _("Print Form of Contract"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "res.partner.contract.wizard",
|
||||
"view_mode": "form",
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"context": {
|
||||
"self_id": self.id,
|
||||
"active_model": self._name,
|
||||
"company_form": self.partner_id.company_form
|
||||
if self.partner_id.is_company
|
||||
else "person",
|
||||
},
|
||||
}
|
||||
|
||||
def get_name_by_document_template(self, document_template_id):
|
||||
self.ensure_one()
|
||||
return self.name
|
||||
|
||||
def get_filename_by_document_template(self, document_template_id):
|
||||
self.ensure_one()
|
||||
return _("{type} {number} from {date}").format(
|
||||
type=_(
|
||||
dict(document_template_id._fields["document_type"].selection).get(
|
||||
document_template_id.document_type
|
||||
)
|
||||
),
|
||||
number=self.name,
|
||||
date=self.get_date().strftime("%d.%m.%Y"),
|
||||
)
|
||||
|
||||
def get_date(self):
|
||||
"""Uses in xml action (data/fields_default)
|
||||
|
||||
Returns:
|
||||
datetime.datetime -- date_conclusion_fix or date_conclusion or create_date
|
||||
"""
|
||||
self.ensure_one()
|
||||
date = self.date_conclusion_fix or self.date_conclusion
|
||||
return date or self.create_date
|
||||
|
||||
@staticmethod
|
||||
def _(arg):
|
||||
"""Uses in xml action (data/fields_default)
|
||||
|
||||
Arguments:
|
||||
arg {str} -- String to translate
|
||||
"""
|
||||
return _(arg)
|
@ -1,236 +0,0 @@
|
||||
import math
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
from ..utils import MODULE_NAME
|
||||
|
||||
# from ..utils.misc import Extension, IDocument
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContractOrderAnnex(models.Model): # , IDocument, Extension):
|
||||
_name = "res.partner.contract.annex"
|
||||
_inherit = ["client_contracts.utils"]
|
||||
_description = "Contract Annex"
|
||||
|
||||
name = fields.Char(
|
||||
string="Name",
|
||||
)
|
||||
display_name = fields.Char(
|
||||
compute="_compute_display_name",
|
||||
)
|
||||
specification_name = fields.Char(
|
||||
compute="_compute_specification_name",
|
||||
)
|
||||
|
||||
contract_id = fields.Many2one(
|
||||
"res.partner.contract",
|
||||
string="Contract",
|
||||
readonly=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
related="contract_id.company_id",
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
related="contract_id.partner_id",
|
||||
)
|
||||
order_id = fields.Many2one(
|
||||
comodel_name="sale.order",
|
||||
string="Sale order",
|
||||
help="Sale order for this annex.",
|
||||
required=True,
|
||||
)
|
||||
date_conclusion = fields.Date(
|
||||
string="Signing Date",
|
||||
default=fields.Date.today(),
|
||||
)
|
||||
counter = fields.Integer(
|
||||
string="№",
|
||||
help="Contract Annexes counter",
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name="res.currency",
|
||||
string="Currency",
|
||||
default=lambda self: self.env.company.currency_id.id,
|
||||
)
|
||||
|
||||
design_period = fields.Integer(
|
||||
string="Design Period",
|
||||
)
|
||||
design_cost = fields.Monetary(
|
||||
string="Design Cost",
|
||||
currency_field="currency_id",
|
||||
)
|
||||
|
||||
design_doc_period = fields.Integer(
|
||||
string="Documentation Design Period (days)",
|
||||
)
|
||||
design_doc_cost = fields.Monetary(
|
||||
string="Documentation Design Cost",
|
||||
currency_field="currency_id",
|
||||
)
|
||||
|
||||
delivery_address = fields.Char(
|
||||
string="Delivery Address",
|
||||
)
|
||||
delivery_period = fields.Integer(string="Delivery Period (days)")
|
||||
|
||||
installation_address = fields.Char(
|
||||
string="Installation Address",
|
||||
)
|
||||
installation_period = fields.Integer(
|
||||
string="Installation Period (days)",
|
||||
)
|
||||
installation_cost = fields.Integer(
|
||||
string="Installation Cost",
|
||||
)
|
||||
|
||||
total_cost = fields.Monetary(
|
||||
string="Total Cost",
|
||||
currency_field="currency_id",
|
||||
)
|
||||
|
||||
payment_part_one = fields.Float(
|
||||
string="Payment 1 Part (%)",
|
||||
default=100,
|
||||
digits="Account",
|
||||
)
|
||||
payment_part_two = fields.Float(
|
||||
string="Payment 2 Part (%)",
|
||||
digits="Account",
|
||||
)
|
||||
payment_part_three = fields.Float(
|
||||
string="Payment 3 Part (%)",
|
||||
digits="Account",
|
||||
)
|
||||
|
||||
@api.depends("name")
|
||||
def _compute_display_name(self):
|
||||
for record in self:
|
||||
record.display_name = "№{} {}".format(
|
||||
record.counter or record.contract_id.contract_annex_number, record.name
|
||||
)
|
||||
|
||||
@api.depends("contract_id", "order_id")
|
||||
def _compute_specification_name(self):
|
||||
self.specification_name = _("{name} from {date}").format(
|
||||
name="{}-{}".format(self.contract_id.name, self.order_id.name),
|
||||
date=self.contract_id.get_date().strftime("%d.%m.%Y"),
|
||||
)
|
||||
|
||||
@api.onchange("order_id")
|
||||
def _domain_order_id(self):
|
||||
"""Using domain function because of
|
||||
simple domain does not working properly because of
|
||||
contract_id is still False"""
|
||||
return {
|
||||
"domain": {
|
||||
"order_id": [
|
||||
("partner_id", "=", self.contract_id.partner_id.id),
|
||||
("contract_annex_id", "=", False),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@api.onchange("order_id")
|
||||
def _onchange_order_id(self):
|
||||
contract_number = self.contract_id.name
|
||||
order_number = self.order_id.name or "SO###"
|
||||
|
||||
self.name = "{contract}-{order}".format(
|
||||
contract=contract_number,
|
||||
order=order_number,
|
||||
)
|
||||
|
||||
def create(self, values_list):
|
||||
_logger.debug("\n\n Values: %s\n\n", values_list)
|
||||
if isinstance(values_list, dict):
|
||||
values_list = [values_list]
|
||||
_logger.debug("\n\n Values fixed: %s\n\n", values_list)
|
||||
records = super(ContractOrderAnnex, self).create(values_list)
|
||||
for record in records:
|
||||
# Fill annex_id to domain it in future
|
||||
# record.order_id.contract_annex_id = record.id
|
||||
# Counter
|
||||
record.counter = record.contract_id.contract_annex_number
|
||||
record.contract_id.contract_annex_number += (
|
||||
1 # TODO: should I use a sequence?
|
||||
)
|
||||
return records
|
||||
|
||||
def action_print_form(self):
|
||||
view = self.env.ref(
|
||||
"{}.res_partner_wizard_print_document_view".format(MODULE_NAME)
|
||||
)
|
||||
return {
|
||||
"name": _("Print Form of Contract Annex"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "res.partner.contract.wizard",
|
||||
"view_mode": "form",
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"context": {
|
||||
"self_id": self.id,
|
||||
"active_model": self._name,
|
||||
"company_form": self.partner_id.company_form
|
||||
if self.partner_id.is_company
|
||||
else "person",
|
||||
},
|
||||
}
|
||||
|
||||
def get_name_by_document_template(self, document_template_id):
|
||||
active_invoices = self.order_id.invoice_ids.filtered(
|
||||
lambda r: r.state not in ("draft", "cancel")
|
||||
)
|
||||
bill_name = active_invoices and active_invoices[-1].number
|
||||
|
||||
return (
|
||||
{
|
||||
"bill": "{bill_name}",
|
||||
"specification": "{counter} {name}",
|
||||
"approval_list": "{counter}.1 {name}-1",
|
||||
"act_at": "{counter}.2 {name}-2",
|
||||
"act_ad": "{counter}.3 {name}-3",
|
||||
}
|
||||
.get(document_template_id.document_type_name, "Unknown")
|
||||
.format(
|
||||
counter=self.counter,
|
||||
name=self.name,
|
||||
bill_name=(bill_name or "Счёт отсутствует"),
|
||||
)
|
||||
)
|
||||
|
||||
def get_filename_by_document_template(self, document_template_id):
|
||||
return "{type} №{name}".format(
|
||||
type=_(
|
||||
dict(document_template_id._fields["document_type"].selection).get(
|
||||
document_template_id.document_type
|
||||
)
|
||||
),
|
||||
name={
|
||||
"bill": "{counter} {type} {name}",
|
||||
"specification": "{counter} {type} {name}",
|
||||
"approval_list": "{counter}.1 {type} {name}-1",
|
||||
"act_at": "{counter}.2 {type} {name}-2",
|
||||
"act_ad": "{counter}.3 {type} {name}-3",
|
||||
}
|
||||
.get(document_template_id.document_type_name, "Unknown")
|
||||
.format(
|
||||
counter=self.counter,
|
||||
type=_(
|
||||
dict(
|
||||
document_template_id._fields["document_type_name"].selection
|
||||
).get(document_template_id.document_type_name)
|
||||
),
|
||||
name=self.name,
|
||||
),
|
||||
)
|
||||
|
||||
def modf(self, arg):
|
||||
"""Math.modf function for using in XML ir.action.server code
|
||||
Uses in data/fields_default.xml
|
||||
"""
|
||||
return math.modf(arg)
|
@ -1,33 +0,0 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ContractField(models.Model):
|
||||
_name = "res.partner.contract.field"
|
||||
_description = "Contract Field"
|
||||
_order = "sequence"
|
||||
|
||||
name = fields.Char(
|
||||
string="Name",
|
||||
required=True,
|
||||
translate=True,
|
||||
)
|
||||
technical_name = fields.Char(
|
||||
string="Technical Name",
|
||||
help="Name for using in templates",
|
||||
required=True,
|
||||
)
|
||||
description = fields.Char(
|
||||
string="Description",
|
||||
help="Description for this field to be showed in fields list in print form creation wizard.",
|
||||
translate=True,
|
||||
default="",
|
||||
)
|
||||
sequence = fields.Integer(
|
||||
string="Sequence",
|
||||
)
|
||||
visible = fields.Boolean(
|
||||
string="Visible",
|
||||
help="To show this field in fields list in print form creation wizard\n"
|
||||
"User can change showed field's values in wizard.",
|
||||
default=True,
|
||||
)
|
@ -1,38 +0,0 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ContractFieldTransient(models.TransientModel):
|
||||
_name = "res.partner.contract.field.transient"
|
||||
_description = "Contract Field Transient"
|
||||
|
||||
_contract_wizard_id = fields.Many2one(
|
||||
"res.partner.contract.wizard",
|
||||
string="Contract",
|
||||
readonly=True,
|
||||
)
|
||||
contract_field_id = fields.Many2one(
|
||||
"res.partner.contract.field",
|
||||
string="Field",
|
||||
)
|
||||
name = fields.Char(
|
||||
related="contract_field_id.name",
|
||||
string="Name",
|
||||
readonly=True,
|
||||
)
|
||||
technical_name = fields.Char(
|
||||
related="contract_field_id.technical_name",
|
||||
string="Technical Name",
|
||||
readonly=True,
|
||||
)
|
||||
description = fields.Char(
|
||||
related="contract_field_id.description",
|
||||
string="Description",
|
||||
readonly=True,
|
||||
)
|
||||
visible = fields.Boolean(
|
||||
related="contract_field_id.visible",
|
||||
)
|
||||
value = fields.Char(
|
||||
string="Value",
|
||||
default="",
|
||||
)
|
@ -1,50 +0,0 @@
|
||||
from odoo import _, fields, models
|
||||
|
||||
|
||||
class DocumentTemplate(models.Model):
|
||||
_name = "res.partner.document.template"
|
||||
_description = "Document Template"
|
||||
_order = "template_type desc,company_type,sequence"
|
||||
|
||||
name = fields.Char()
|
||||
attachment_id = fields.Many2one(
|
||||
"ir.attachment",
|
||||
string="Template Attachment",
|
||||
ondelete="cascade",
|
||||
required=True,
|
||||
)
|
||||
document_type = fields.Selection(
|
||||
string="Type of document",
|
||||
selection=[
|
||||
("contract", _("Contract")),
|
||||
("annex", _("Annex")),
|
||||
("addition", _("Addition")),
|
||||
("offer", _("Offer")),
|
||||
],
|
||||
)
|
||||
document_type_name = fields.Selection(
|
||||
string="Document",
|
||||
selection=[
|
||||
("offer", _("Offer")),
|
||||
("bill", _("Bill")),
|
||||
("specification", _("Specification")),
|
||||
("approval_list", _("Approval List")),
|
||||
("act_at", _("Act of Acceptance and Transfer")),
|
||||
("act_ad", _("Act of Acceptance and Delivery")),
|
||||
],
|
||||
)
|
||||
company_type = fields.Selection(
|
||||
selection=[
|
||||
("person", "Individual"),
|
||||
("sp", "Sole Proprietor"),
|
||||
("plc", "Private Limited Company"),
|
||||
]
|
||||
)
|
||||
template_type = fields.Selection(
|
||||
selection=[
|
||||
("contract", "Contract"),
|
||||
("annex", "Annex"),
|
||||
("offer", _("Offer")),
|
||||
]
|
||||
)
|
||||
sequence = fields.Integer()
|
@ -1,95 +0,0 @@
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
from ..utils import MODULE_NAME
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
# TODO: exists original field "commitment_date".
|
||||
delivery_time = fields.Integer(
|
||||
string="Delivery Time",
|
||||
default=45,
|
||||
)
|
||||
contract_annex_id = fields.Many2one(
|
||||
comodel_name="res.partner.contract.annex",
|
||||
string="Contract Annex",
|
||||
compute="get_contract_annex_id",
|
||||
)
|
||||
contract_annex_ids = fields.One2many(
|
||||
comodel_name="res.partner.contract.annex",
|
||||
inverse_name="order_id",
|
||||
string="Annex for this Sale order",
|
||||
help="Technical field for binding with contract annex\n"
|
||||
"In form this link showed in 'contract_annex_id' field.",
|
||||
)
|
||||
|
||||
# Extend default field for showing payment terms created by this module only.
|
||||
payment_term_id = fields.Many2one(
|
||||
comodel_name="account.payment.term",
|
||||
domain=lambda self: [("id", "in", self._get_payment_terms())],
|
||||
)
|
||||
|
||||
def _get_payment_terms(self):
|
||||
terms = [
|
||||
self.env.ref("{}.{}".format(MODULE_NAME, external_id)).id
|
||||
for external_id in (
|
||||
"payment_term_prepaid",
|
||||
"payment_term_postpayment",
|
||||
"payment_term_partial_2",
|
||||
"payment_term_partial_3",
|
||||
)
|
||||
]
|
||||
return terms
|
||||
|
||||
@api.onchange("contract_annex_ids")
|
||||
def get_contract_annex_id(self):
|
||||
if self.contract_annex_ids:
|
||||
self.contract_annex_id = self.contract_annex_ids[0].id
|
||||
else:
|
||||
self.contract_annex_id = False
|
||||
|
||||
def action_print_form(self):
|
||||
self.ensure_one()
|
||||
view = self.env.ref(
|
||||
"{}.res_partner_wizard_print_document_view".format(MODULE_NAME)
|
||||
)
|
||||
return {
|
||||
"name": _("Print Form of Contract"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "res.partner.contract.wizard",
|
||||
"view_mode": "form",
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"context": {
|
||||
"self_id": self.id,
|
||||
"active_model": self._name,
|
||||
"company_form": self.partner_id.company_form
|
||||
if self.partner_id.is_company
|
||||
else "person",
|
||||
"attachment_model": "sale.order",
|
||||
},
|
||||
}
|
||||
|
||||
def get_filename_by_document_template(self, document_template_id):
|
||||
self.ensure_one()
|
||||
return "{doc_type} {number} {from_} {date}".format(
|
||||
doc_type=_("Offer"),
|
||||
number=self.name,
|
||||
from_=_("from"),
|
||||
date=self.date_order.strftime("%d.%m.%Y"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _t(arg):
|
||||
"""Uses in xml action (data/fields_default)
|
||||
Arguments:
|
||||
arg {str} -- String to translate
|
||||
"""
|
||||
return _(arg)
|
||||
|
||||
@staticmethod
|
||||
def to_fixed(number, digit=2):
|
||||
if isinstance(number, str) and number.isdigit():
|
||||
number = float(number)
|
||||
return f"{number:.{digit}f}"
|
@ -1,12 +0,0 @@
|
||||
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
||||
access_contracts_contracts,access_contracts_contracts,model_res_partner_contract,base.group_user,1,1,1,1
|
||||
|
||||
access_contracts_field,access_contracts_field,model_res_partner_contract_field,base.group_user,1,0,0,0
|
||||
access_contracts_field_manager,access_contracts_field,model_res_partner_contract_field,base.group_no_one,1,1,1,1
|
||||
|
||||
access_contracts_field_transient,access_contracts_field_transient,model_res_partner_contract_field_transient,base.group_user,1,1,1,1
|
||||
access_contracts_field_contract_annex,access_contracts_field_contract_annex,model_res_partner_contract_annex,base.group_user,1,1,1,1
|
||||
access_contract_document_template,access_contract_document_template,model_res_partner_document_template,base.group_user,1,0,0,0
|
||||
access_contract_document_template_manager,access_contract_document_template,model_res_partner_document_template,base.group_no_one,1,1,1,1
|
||||
access_contracts_wizard,access_contracts_wizard,model_res_partner_contract_wizard,base.group_user,1,1,1,1
|
|
@ -1,3 +1 @@
|
||||
from . import misc
|
||||
|
||||
MODULE_NAME = __package__.split(".")[-2]
|
||||
|
@ -1,26 +0,0 @@
|
||||
import io
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
from jinja2 import Environment as Jinja2Environment
|
||||
|
||||
from .num2words import num2words_, num2words_currency
|
||||
|
||||
|
||||
def get_document_from_values_stream(path_to_template: str, vals: dict):
|
||||
doc = DocxTemplate(path_to_template)
|
||||
|
||||
jinja_env = Jinja2Environment()
|
||||
|
||||
functions = {
|
||||
"number2words": num2words_,
|
||||
"currency2words": num2words_currency,
|
||||
}
|
||||
jinja_env.globals.update(**functions)
|
||||
|
||||
doc.render(vals, jinja_env)
|
||||
|
||||
file_stream = io.BytesIO()
|
||||
doc.save(file_stream)
|
||||
file_stream.seek(0)
|
||||
|
||||
return file_stream
|
@ -1,11 +0,0 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Utils(models.AbstractModel):
|
||||
_name = "client_contracts.utils"
|
||||
|
||||
@staticmethod
|
||||
def to_fixed(number, digit=2):
|
||||
if isinstance(number, str) and number.isdigit():
|
||||
number = float(number)
|
||||
return f"{number:.{digit}f}"
|
18
views/ir_actions_report_views.xml
Executable file
18
views/ir_actions_report_views.xml
Executable file
@ -0,0 +1,18 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
|
||||
<record id="act_report_xml_inherit_view_form" model="ir.ui.view">
|
||||
<field name="name">ir.actions.report.inherit.view.form</field>
|
||||
<field name="model">ir.actions.report</field>
|
||||
<field name="inherit_id" ref="base.act_report_xml_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='report_name']" position="attributes">
|
||||
<attribute name="attrs">{'required': [('report_type', 'not in', ['docx-docx'])]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='report_name']" position="after">
|
||||
<field name="report_docx_template" attrs="{'required': [('report_type', 'in', ['docx-docx'])]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
@ -1,92 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- res.partner.contract.field action window -->
|
||||
<record id="res_partner_contract_field_action" model="ir.actions.act_window">
|
||||
<field name="name">Contract Fields</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.partner.contract.field</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<!-- res.partner.document.template action window -->
|
||||
<record id="res_partner_document_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Document Templates</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.partner.document.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_contract_info_inherit_view" model="ir.ui.view">
|
||||
<field name="name">res.partner.contract.info</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="priority" eval="25"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//field[@name='name']" position="attributes">
|
||||
<attribute name="placeholder">Name in ERP</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='type']" position="before">
|
||||
<field name="name_write" placeholder="i.e. Ural Bank for Reconstruction and Development" attrs="{'invisible': [('is_company', '=', False)]}"/>
|
||||
<field name="name_genitive" attrs="{'invisible': [('is_company', '=', True)]}"/>
|
||||
<field name="name_initials" attrs="{'invisible': [('is_company', '=', True)]}"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='function']" position="after">
|
||||
<field name="function_genitive" attrs="{'invisible': [('is_company', '=', True)]}"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='category_id']" position="after">
|
||||
<field name="representative_id" domain="[('is_company', '=', False)]" attrs="{'invisible': [('is_company', '=', False)]}"/>
|
||||
<field name="representative_document" attrs="{'invisible': [('is_company', '=', False)]}"/>
|
||||
|
||||
<field name="signature" widget="image"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='function']" position="after">
|
||||
<field name="function_genitive" attrs="{'invisible': [('is_company','=', True)]}" invisible="1"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='email']" position="before">
|
||||
<field name="whatsapp" attrs="{'invisible': [('is_company', '=', True)]}"/>
|
||||
<field name="telegram" attrs="{'invisible': [('is_company', '=', True)]}"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_contract_view_buttons" model="ir.ui.view">
|
||||
<field name="name">res.partner.contract.view.buttons</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="priority" eval="25"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_inline oe_stat_button" type="action" name="%(res_partner_contract_partner_action)d" attrs="{'invisible': [('parent_id', '!=', False)]}" icon="fa-pencil-square-o">
|
||||
<field string="Contracts" name="contract_count" widget="statinfo"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<menuitem id="res_partner_menu_contracts"
|
||||
name="Contract"
|
||||
parent="contacts.res_partner_menu_config"
|
||||
sequence="7"/>
|
||||
|
||||
<menuitem id="res_partner_menu_contracts_fields"
|
||||
name="Fields"
|
||||
action="res_partner_contract_field_action"
|
||||
parent="res_partner_menu_contracts"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem id="res_partner_menu_contracts_templates"
|
||||
name="Templates"
|
||||
parent="res_partner_menu_contracts"
|
||||
action="res_partner_document_template_action"
|
||||
sequence="2"/>
|
||||
|
||||
</odoo>
|
@ -1,106 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
|
||||
<!-- res.partner.contract tree -->
|
||||
<record id="res_partner_contract_tree" model="ir.ui.view">
|
||||
<field name="name">Contracts</field>
|
||||
<field name="model">res.partner.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Contracts">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date_conclusion"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- res.partner.contract form -->
|
||||
<record id="res_partner_contract_form" model="ir.ui.view">
|
||||
<field name="name">Contract Form</field>
|
||||
<field name="model">res.partner.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Contract" create="0">
|
||||
<header attrs="{'invisible': [('id', '=', False)]}">
|
||||
<button name="action_sign" type="object" string="Sign" states="draft" class="oe_highlight"/>
|
||||
<button name="action_close" type="object" string="Close" states="sign" class="oe_highlight"/>
|
||||
<button name="action_renew" type="object" string="Renew" states="close"/>
|
||||
|
||||
<button name="action_print_form" type="object" string="Print"/>
|
||||
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,sign,close"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group string="Contract parameters" name="single_params">
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
<field name="create_date" readonly="1" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
<field name="date_conclusion" readonly="1" attrs="{'invisible': ['|', ('id', '=', False), ('date_conclusion', '=', False)]}"/>
|
||||
<field name="date_conclusion_fix" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
|
||||
<!-- Uses to generate number of Annex -->
|
||||
<field name="contract_annex_number" invisible="1"/>
|
||||
</group>
|
||||
<group string="Annexed Specifications" name="annexes"> <!--attrs="{'invisible': ['|', ('id', '=', False), ('state', 'in', 'draft')]}"-->
|
||||
<field name="contract_annex_ids"
|
||||
nolabel="1"
|
||||
mode="tree"
|
||||
attrs="{'readonly': ['|', ('id', '=', False), ('state', 'in', 'draft')]}"
|
||||
>
|
||||
<tree>
|
||||
<field name="contract_id" invisible="1"/>
|
||||
<field name="display_name" string="Name"/>
|
||||
<button name="action_print_form" type="object" string="Print" attrs="{'invisible': [('id', '=', False)]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Mail Thread -->
|
||||
<!-- res.partner.contract inherit form -->
|
||||
<record id="view_id" model="ir.ui.view">
|
||||
<field name="name">res.partner.contract.inherit.view.form</field>
|
||||
<field name="model">res.partner.contract</field>
|
||||
<field name="inherit_id" ref="res_partner_contract_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//sheet" position="after">
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
<field name="activity_ids" widget="mail_activity"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Need it? -->
|
||||
<record id="search_res_partner_contract_filter" model="ir.ui.view">
|
||||
<field name="name">res_partner_contract_search</field>
|
||||
<field name="model">res.partner.contract</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Contract">
|
||||
<field name="partner_id" operator="child_of"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_contract_action" model="ir.actions.act_window">
|
||||
<field name="name">Contracts</field>
|
||||
<field name="res_model">res.partner.contract</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_contract_partner_action" model="ir.actions.act_window">
|
||||
<field name="name">Contracts</field>
|
||||
<field name="res_model">res.partner.contract</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_partner_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
@ -1,51 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- res.partner.contract.annex form view -->
|
||||
<record id="res_partner_contract_annex_view_form" model="ir.ui.view">
|
||||
<field name="name">res.partner.contract.annex.view.form</field>
|
||||
<field name="model">res.partner.contract.annex</field>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<form string="Contract Annex">
|
||||
<sheet>
|
||||
<group name="options" invisible="1">
|
||||
<field name="display_name" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
<group name="info" string="Info">
|
||||
<field name="name" placeholder="Leave empty for compute"/>
|
||||
<field name="contract_id" attrs="{'invisible': [('contract_id', '=', False)]}"/>
|
||||
<field name="order_id" options="{'no_create': True}"/>
|
||||
<field name="date_conclusion"/>
|
||||
</group>
|
||||
<group name="design" string="Design">
|
||||
<field name="design_period"/>
|
||||
<field name="design_cost"/>
|
||||
<field name="design_doc_period"/>
|
||||
<field name="design_doc_cost"/>
|
||||
</group>
|
||||
<group name="delivery" string="Delivery">
|
||||
<field name="delivery_address"/>
|
||||
<field name="delivery_period"/>
|
||||
</group>
|
||||
<group name="installation" string="Installation">
|
||||
<field name="installation_address"/>
|
||||
<field name="installation_period"/>
|
||||
<field name="installation_cost"/>
|
||||
</group>
|
||||
<group name="payment" string="Payment">
|
||||
<field name="total_cost"/>
|
||||
<field name="payment_part_one" widget="integer"/>
|
||||
<field name="payment_part_two" widget="integer"/>
|
||||
<field name="payment_part_three" widget="integer"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
@ -1,20 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- res.partner.contract.field tree view -->
|
||||
<record id="res_partner_contract_field_view_tree" model="ir.ui.view">
|
||||
<field name="name">res.partner.contract.field.view.tree</field>
|
||||
<field name="model">res.partner.contract.field</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="technical_name"/>
|
||||
<field name="visible"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
@ -1,17 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
|
||||
<!-- res.partner.document.template tree view -->
|
||||
<record id="res_partner_document_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">res.partner.document.template.view.tree</field>
|
||||
<field name="model">res.partner.document.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="company_type"/>
|
||||
<field name="document_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
@ -1,25 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
|
||||
<!-- sale.order inherit form view -->
|
||||
<record id="view_order_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.inherit.view.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_print_form" type="object" string="Create offer document"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='payment_term_id']" position="after">
|
||||
<field name="delivery_time"/>
|
||||
<field name="contract_annex_id" readonly="1"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="res_partner_contract_menu_act" name="Contracts" parent="sale.sale_order_menu" action="res_partner_contract_action" sequence="4"/>
|
||||
|
||||
</odoo>
|
@ -1 +0,0 @@
|
||||
from . import res_partner_contract_wizard
|
@ -1,369 +0,0 @@
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
from ..utils import MODULE_NAME
|
||||
from ..utils.docxtpl import get_document_from_values_stream
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContractWizard(models.TransientModel):
|
||||
_name = "res.partner.contract.wizard"
|
||||
_inherit = ["client_contracts.utils"]
|
||||
|
||||
def _default_target(self):
|
||||
return "{model},{target_id}".format(
|
||||
model=self.env.context.get("active_model"),
|
||||
target_id=int(self.env.context.get("self_id")),
|
||||
)
|
||||
|
||||
def _default_document_template(self):
|
||||
return self.env["res.partner.document.template"].search(
|
||||
self._get_template_domain(), limit=1
|
||||
)
|
||||
|
||||
def _get_template_domain(self):
|
||||
template_type = {
|
||||
"res.partner.contract": "contract",
|
||||
"res.partner.contract.annex": "annex",
|
||||
"sale.order": "offer",
|
||||
}.get(self.active_model, False)
|
||||
company_type = self.env.context.get("company_form", False)
|
||||
document_template_domain = [
|
||||
("template_type", "=", template_type),
|
||||
("company_type", "=", company_type),
|
||||
]
|
||||
return document_template_domain
|
||||
|
||||
target = fields.Reference(
|
||||
selection=[
|
||||
("res.partner.contract", "Contract"),
|
||||
("res.partner.contract.annex", "Contract Annex"),
|
||||
("sale.order", "Offer"),
|
||||
],
|
||||
string="Target",
|
||||
default=_default_target,
|
||||
help="Record of contract or annex entity, from where wizard has been called",
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Company",
|
||||
compute="_compute_company_id",
|
||||
)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Partner",
|
||||
compute="_compute_partner_id",
|
||||
)
|
||||
document_name = fields.Char(
|
||||
string="Document Name", compute="_compute_document_name"
|
||||
)
|
||||
document_template = fields.Many2one(
|
||||
comodel_name="res.partner.document.template",
|
||||
string="Document Template",
|
||||
default=_default_document_template,
|
||||
domain=lambda self: self._get_template_domain(),
|
||||
readonly=False,
|
||||
)
|
||||
transient_field_ids = fields.One2many(
|
||||
comodel_name="res.partner.contract.field.transient",
|
||||
inverse_name="_contract_wizard_id",
|
||||
string="Contract Fields",
|
||||
)
|
||||
transient_field_ids_hidden = fields.One2many(
|
||||
comodel_name="res.partner.contract.field.transient",
|
||||
inverse_name="_contract_wizard_id",
|
||||
)
|
||||
|
||||
@api.depends("target")
|
||||
def _compute_company_id(self):
|
||||
if self.target and self.target.company_id:
|
||||
self.company_id = self.target.company_id.id
|
||||
else:
|
||||
self.company_id = self.env.company.id
|
||||
|
||||
@api.depends("target")
|
||||
def _compute_partner_id(self):
|
||||
if self.target:
|
||||
self.partner_id = self.target.partner_id
|
||||
|
||||
@api.depends("document_template", "target")
|
||||
def _compute_document_name(self):
|
||||
self.document_name = self.target.get_name_by_document_template(
|
||||
self.document_template
|
||||
)
|
||||
|
||||
@api.constrains("document_template")
|
||||
def _check_document_template(self):
|
||||
if not self.document_template:
|
||||
raise ValidationError("You did not set up the template...")
|
||||
|
||||
@api.onchange("document_template")
|
||||
def _domain_document_template(self):
|
||||
return {
|
||||
"domain": {
|
||||
"document_template": self._get_template_domain(),
|
||||
}
|
||||
}
|
||||
|
||||
@api.onchange("document_template")
|
||||
def _onchange_document_template(self):
|
||||
"""Creates transient fields for generate contract template
|
||||
Looks as a tree view of *_contract_field_transient model in xml
|
||||
"""
|
||||
|
||||
def get_contract_field_data(field_name, field_value):
|
||||
rec = self.env["res.partner.contract.field"].search(
|
||||
[("technical_name", "=", field_name)]
|
||||
)
|
||||
if not rec:
|
||||
raise UserError(
|
||||
_(
|
||||
'Field "%s" specified in template, not found in model "res.partner.contract.field"'
|
||||
)
|
||||
% field_name
|
||||
)
|
||||
return {
|
||||
"contract_field_id": rec.id,
|
||||
"visible": rec.visible,
|
||||
"value": field_value,
|
||||
}
|
||||
|
||||
model_to_action = {
|
||||
"res.partner.contract": "action_get_contract_context",
|
||||
"res.partner.contract.annex": "action_get_annex_context",
|
||||
"sale.order": "action_get_so_context",
|
||||
}
|
||||
action_external_id = "{}.{}".format(
|
||||
MODULE_NAME, model_to_action[self.active_model]
|
||||
)
|
||||
action_rec = self.env.ref(action_external_id)
|
||||
action_rec.model_id = (
|
||||
self.env["ir.model"].search([("model", "=", self.active_model)]).id
|
||||
)
|
||||
|
||||
# Get dictionary for `transient_fields_ids` with editable fields
|
||||
# With data from Odoo database
|
||||
contract_context_values = action_rec.with_context(
|
||||
{"onchange_self": self.target}
|
||||
).run()
|
||||
|
||||
transient_fields_data = [
|
||||
get_contract_field_data(field_name, field_value)
|
||||
for field_name, field_value in contract_context_values.items()
|
||||
]
|
||||
transient_fields_hidden_data = list(
|
||||
filter(lambda item: not item["visible"], transient_fields_data)
|
||||
)
|
||||
transient_fields_data = list(
|
||||
filter(lambda item: item["visible"], transient_fields_data)
|
||||
)
|
||||
|
||||
self.transient_field_ids = [
|
||||
(
|
||||
6,
|
||||
False,
|
||||
self.env["res.partner.contract.field.transient"]
|
||||
.create(transient_fields_data)
|
||||
.ids,
|
||||
)
|
||||
]
|
||||
self.transient_field_ids_hidden = [
|
||||
(
|
||||
6,
|
||||
False,
|
||||
self.env["res.partner.contract.field.transient"]
|
||||
.create(transient_fields_hidden_data)
|
||||
.ids,
|
||||
)
|
||||
]
|
||||
|
||||
# Other
|
||||
def get_docx_contract(self):
|
||||
template = self.document_template.attachment_id
|
||||
template_path = template._full_path(template.store_fname)
|
||||
|
||||
payload = self.payload()
|
||||
binary_data = get_document_from_values_stream(template_path, payload).read()
|
||||
encoded_data = base64.b64encode(binary_data)
|
||||
|
||||
get_fn = self.target.get_filename_by_document_template
|
||||
attachment_name = "{}.docx".format(get_fn(self.document_template or "Unknown"))
|
||||
|
||||
document_as_attachment = (
|
||||
self.env["ir.attachment"]
|
||||
.sudo()
|
||||
.create(
|
||||
{
|
||||
"name": attachment_name,
|
||||
"store_fname": attachment_name,
|
||||
"type": "binary",
|
||||
"datas": encoded_data,
|
||||
}
|
||||
)
|
||||
)
|
||||
return self.afterload(document_as_attachment)
|
||||
|
||||
def get_so_lines(self):
|
||||
"""
|
||||
Generates lines for printing from Sale order lines, including folding groups
|
||||
ended with text "--fold".
|
||||
:return: 2 values: lines data with folded groups and total amount of SO.
|
||||
"""
|
||||
|
||||
def number_generator(n=1):
|
||||
while True:
|
||||
yield n
|
||||
n += 1
|
||||
|
||||
sale_order_rec = (
|
||||
self.target if self.target._name == "sale.order" else self.target.order_id
|
||||
)
|
||||
counter = number_generator()
|
||||
lines_data = []
|
||||
folded_group = False
|
||||
group_description = ""
|
||||
group_amount = 0.0
|
||||
|
||||
for item in sale_order_rec.order_line:
|
||||
# Folded group ends #
|
||||
if item.display_type == "line_section" and folded_group:
|
||||
folded_group = False
|
||||
lines_data.append(
|
||||
{
|
||||
"number": next(counter),
|
||||
"vendor_code": "",
|
||||
"label": group_description,
|
||||
"count": 1.0,
|
||||
"unit": self.env.ref("uom.product_uom_unit").name,
|
||||
"cost_wo_vat": self.to_fixed(group_amount),
|
||||
"subtotal": self.to_fixed(group_amount),
|
||||
"display_type": False,
|
||||
}
|
||||
)
|
||||
# Folded group starts #
|
||||
if item.display_type == "line_section" and item.name.find("--fold") >= 0:
|
||||
folded_group = True
|
||||
group_amount = 0.0
|
||||
index_for_cut = item.name.find("--fold")
|
||||
group_description = item.name[:index_for_cut].strip()
|
||||
# Regular, unfolded group or comment or regular line with product #
|
||||
if (
|
||||
item.display_type == "line_note"
|
||||
or item.display_type == "line_section"
|
||||
and item.name.find("--fold") == -1
|
||||
or not item.display_type
|
||||
) and not folded_group:
|
||||
lines_data.append(
|
||||
{
|
||||
"number": next(counter) if not item.display_type else "",
|
||||
"vendor_code": item.product_id.default_code
|
||||
if (item.product_id and item.product_id.default_code)
|
||||
else "",
|
||||
"label": item.product_id.display_name
|
||||
if item.product_id
|
||||
else "",
|
||||
"description": item.name,
|
||||
"count": item.product_uom_qty,
|
||||
"unit": item.product_uom.name if item.product_uom else "",
|
||||
"cost": self.to_fixed(item.price_unit),
|
||||
"cost_wo_vat": self.to_fixed(item.price_reduce_taxexcl),
|
||||
"discount": item.discount,
|
||||
"subtotal": self.to_fixed(item.price_subtotal),
|
||||
"display_type": item.display_type,
|
||||
}
|
||||
)
|
||||
# Line with product or comment inside folded group #
|
||||
if folded_group and not item.display_type:
|
||||
group_amount += item.price_subtotal
|
||||
# Last folded group handling #
|
||||
if folded_group and group_description:
|
||||
lines_data.append(
|
||||
{
|
||||
"number": next(counter),
|
||||
"vendor_code": "",
|
||||
"label": group_description,
|
||||
"count": 1.0,
|
||||
"unit": self.env.ref("uom.product_uom_unit").name,
|
||||
"cost_wo_vat": self.to_fixed(group_amount),
|
||||
"subtotal": self.to_fixed(group_amount),
|
||||
"display_type": False,
|
||||
}
|
||||
)
|
||||
return lines_data, sale_order_rec.amount_total
|
||||
|
||||
def payload(self):
|
||||
# Collect fields into a key-value structure
|
||||
fields = {
|
||||
transient_field.technical_name: transient_field.value
|
||||
for transient_field in (
|
||||
self.transient_field_ids + self.transient_field_ids_hidden
|
||||
)
|
||||
}
|
||||
# Extend with special case
|
||||
if self.target._name == "res.partner.contract.annex":
|
||||
fields.update(
|
||||
{
|
||||
"annex_name": self.document_name,
|
||||
"specification_name": self.target.specification_name,
|
||||
}
|
||||
)
|
||||
# Extend with order product lines
|
||||
if (
|
||||
self.target._name == "sale.order"
|
||||
or hasattr(self.target, "order_id")
|
||||
and self.target.order_id.order_line
|
||||
):
|
||||
so_lines, total_amount = self.get_so_lines()
|
||||
fields.update(
|
||||
{
|
||||
"products": so_lines,
|
||||
"total_amount": total_amount,
|
||||
"products_amount": len(
|
||||
list(filter(lambda rec: not rec["display_type"], so_lines))
|
||||
),
|
||||
}
|
||||
)
|
||||
return self.middleware_fields(fields)
|
||||
|
||||
def afterload(self, result):
|
||||
res_id = self.target.id
|
||||
if hasattr(self.target, "contract_id"):
|
||||
res_id = self.target.contract_id.id
|
||||
target_model = (
|
||||
self.target._name
|
||||
if self.target._name
|
||||
not in ("res.partner.contract", "res.partner.contract.annex")
|
||||
else "res.partner.contract"
|
||||
)
|
||||
self.env["mail.message"].create(
|
||||
{
|
||||
"model": self.env.context.get("attachment_model") or target_model,
|
||||
"res_id": self.env.context.get("attachment_res_id", res_id),
|
||||
"message_type": "comment",
|
||||
"attachment_ids": [(4, result.id, False)],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def middleware_fields(kv):
|
||||
"""
|
||||
Removes items without values from dictionary.
|
||||
:kv: dict.
|
||||
"""
|
||||
# Debug False values
|
||||
empty = []
|
||||
for k, v in list(kv.items()):
|
||||
if not v:
|
||||
empty.append(k)
|
||||
kv.pop(k)
|
||||
_logger.debug("Empty fields: {}".format(empty))
|
||||
return kv
|
||||
|
||||
@property
|
||||
def active_model(self):
|
||||
return self.env.context.get("active_model")
|
@ -1,42 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_partner_wizard_print_document_view" model="ir.ui.view">
|
||||
<field name="name">Generate Document</field>
|
||||
<field name="model">res.partner.contract.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<form>
|
||||
|
||||
<group string="Template">
|
||||
<field name="document_template"></field>
|
||||
</group>
|
||||
|
||||
<group invisible="1">
|
||||
<!-- Need for generate a document -->
|
||||
<field name="target"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="company_id"/>
|
||||
</group>
|
||||
|
||||
<button string="Create a document" type="object" name="get_docx_contract" />
|
||||
|
||||
<group string="Values">
|
||||
<field name="transient_field_ids" nolabel="1" colspan="4">
|
||||
<tree editable="1" create="0" delete="0">
|
||||
<field name="name"/>
|
||||
<field name="value"/>
|
||||
<field name="description"/>
|
||||
<field name="technical_name" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="transient_field_ids_hidden" invisible="1"/>
|
||||
</group>
|
||||
|
||||
<footer/>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
BIN
Шаблон_КП.docx
BIN
Шаблон_КП.docx
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user