[MIG] contract: Migration to 15.0

Most changes are related to the switch from jinja to qweb in mail templates.

Also included:
- convert deprecated onchange that returns a domain and other deprecation warnings
  (see below)
- Add migration scripts from version 14.0 (force the update of the mail templates)
- Fix warnings from pre-commit checks

Fixes depreciation warnings:

- onchange method ContractAbstractContractLine._onchange_product_id returned
  a domain, this is deprecated
- SavepointCase is deprecated:
  https://github.com/odoo/odoo/blob/15.0/odoo/tests/common.py#L742
- assertDictContainsSubset: According to:
  https://stackoverflow.com/questions/20050913/python-unittests-assertdictcontainssubset-recommended-alternative
This commit is contained in:
Jean-Charles Drubay
2021-10-28 18:24:51 +07:00
parent 7537451d2f
commit 119b5b6700
19 changed files with 377 additions and 232 deletions

View File

@@ -11,7 +11,7 @@
{ {
"name": "Recurring - Contracts Management", "name": "Recurring - Contracts Management",
"version": "14.0.1.2.3", "version": "15.0.1.0.0",
"category": "Contract Management", "category": "Contract Management",
"license": "AGPL-3", "license": "AGPL-3",
"author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)", "author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)",
@@ -35,7 +35,6 @@
"wizards/contract_manually_create_invoice.xml", "wizards/contract_manually_create_invoice.xml",
"wizards/contract_contract_terminate.xml", "wizards/contract_contract_terminate.xml",
"views/contract_tag.xml", "views/contract_tag.xml",
"views/assets.xml",
"views/abstract_contract_line.xml", "views/abstract_contract_line.xml",
"views/contract.xml", "views/contract.xml",
"views/contract_line.xml", "views/contract_line.xml",
@@ -46,5 +45,12 @@
"views/contract_terminate_reason.xml", "views/contract_terminate_reason.xml",
"views/contract_portal_templates.xml", "views/contract_portal_templates.xml",
], ],
"assets": {
"web.assets_backend": [
"contract/static/src/js/section_and_note_fields_backend.js",
],
"web.assets_frontend": ["contract/static/src/scss/frontend.scss"],
"web.assets_tests": ["contract/static/src/js/contract_portal_tour.js"],
},
"installable": True, "installable": True,
} }

View File

@@ -4,93 +4,121 @@
<field name="name">Email Contract Template</field> <field name="name">Email Contract Template</field>
<field <field
name="email_from" name="email_from"
>${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field> >{{ (object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '') }}</field>
<field <field
name="subject" name="subject"
>${object.company_id.name} Contract (Ref ${object.name or 'n/a'})</field> >{{ object.company_id.name }} Contract (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">${object.partner_id.id}</field> <field name="partner_to">{{ object.partner_id.id }}</field>
<field name="model_id" ref="model_contract_contract" /> <field name="model_id" ref="model_contract_contract" />
<field name="auto_delete" eval="True" /> <field name="auto_delete" eval="True" />
<field name="report_template" ref="contract.report_contract" /> <field name="report_template" ref="contract.report_contract" />
<field name="report_name">Contract</field> <field name="report_name">Contract</field>
<field name="lang">${object.partner_id.lang}</field> <field name="lang">{{ object.partner_id.lang }}</field>
<field <field name="body_html" type="html">
name="body_html" <div
><![CDATA[ style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; "
<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; "> >
<p>Hello ${object.partner_id.name or ''},</p> <p>Hello <t t-out="object.partner_id.name or '' " />,</p>
<p>A new contract has been created: </p> <p>A new contract has been created: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;"> <p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br /> &amp;nbsp;&amp;nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Contract: <strong>${object.name}</strong><br /> &amp;nbsp;&amp;nbsp;Contract: <strong t-out="object.name" /><br />
% if object.date_start: <t t-if="object.date_start">
&nbsp;&nbsp;Contract Date Start: ${object.date_start or ''}<br /> &amp;nbsp;&amp;nbsp;Contract Date Start: <t
% endif t-out="object.date_start or ''"
/><br />
</t>
% if object.user_id: <t t-if="object.user_id">
% if object.user_id.email: <t t-if="object.user_id.email">
&nbsp;&nbsp;Your Contact: <a href="mailto:${object.user_id.email or ''}?subject=Contract%20${object.name}">${object.user_id.name}</a> &amp;nbsp;&amp;nbsp;Your Contact: <a
% else: t-att-href="'mailto:%s?subject=Contract %s' % (object.user_id.email, object.name)"
&nbsp;&nbsp;Your Contact: ${object.user_id.name} t-out="object.user_id.name"
% endif />
% endif </t>
<t t-else="">
&amp;nbsp;&amp;nbsp;Your Contact: <t
t-out="object.user_id.name"
/>
</t>
</t>
</p> </p>
<br/> <br />
<p>If you have any questions, do not hesitate to contact us.</p> <p>If you have any questions, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p> <p>Thank you for choosing <t
<br/> t-out="object.company_id.name or 'us'"
<br/> />!</p>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;"> <br />
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;"> <br />
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3> <div
style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;"
>
<h3
style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;"
>
<strong
style="text-transform:uppercase;"
t-out="object.company_id.name"
/></h3>
</div> </div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;"> <div
style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;"
>
<span style="color: #222; margin-bottom: 5px; display: block; "> <span style="color: #222; margin-bottom: 5px; display: block; ">
${object.company_id.partner_id.sudo().with_context(show_address=True, html_format=True).name_get()[0][1] | safe} <address
t-field="object.company_id.sudo().partner_id"
t-options='{"widget": "contact", "fields": ["name", "address"], "no_marker": True}'
/>
</span> </span>
% if object.company_id.phone: <t t-if="object.company_id.phone">
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "> <div
Phone: ${object.company_id.phone} style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "
>
Phone: <t t-out="object.company_id.phone" />
</div> </div>
% endif </t>
% if object.company_id.website: <t t-if="object.company_id.website">
<div> <div>
Web: <a href="${object.company_id.website}">${object.company_id.website}</a> Web: <a
t-att-href="object.company_id.website"
t-out="object.company_id.website"
/>
</div> </div>
%endif </t>
<p></p>
</div> </div>
<p></p> <br />
<a href="${object.get_base_url()}/my/contracts/${object.id}?access_token=${object.access_token}" target="_blank" style="background-color:#875A7B;padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">View contract</a> <a
</div> t-att-href="'%s/my/contracts/%s?access_token=%s' % (object.get_base_url(), object.id, object.access_token)"
]]></field> target="_blank"
style="background-color:#875A7B;padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;"
>View contract</a>
</div>
</field>
</record> </record>
<record id="mail_template_contract_modification" model="mail.template"> <record id="mail_template_contract_modification" model="mail.template">
<field name="name">Contract Modification Template</field> <field name="name">Contract Modification Template</field>
<field <field
name="email_from" name="email_from"
>${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field> >{{ (object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '') }}</field>
<field <field
name="subject" name="subject"
>${object.company_id.name} Contract (Ref ${object.name or 'n/a'}) - Modifications</field> >{{ object.company_id.name }} Contract (Ref {{ object.name or 'n/a' }}) - Modifications</field>
<field name="model_id" ref="model_contract_contract" /> <field name="model_id" ref="model_contract_contract" />
<field name="lang">${object.partner_id.lang}</field> <field name="lang">{{ object.partner_id.lang }}</field>
<field <field name="body_html" type="html">
name="body_html"
><![CDATA[
<p>Hello</p> <p>Hello</p>
<p>We have modifications on the contract that we want to notify you.</p> <p>We have modifications on the contract that we want to notify you.</p>
]]></field> </field>
</record> </record>
<template <template
id="mail_notification_contract" id="mail_notification_contract"
inherit_id="mail.mail_notification_paynow" inherit_id="mail.mail_notification_paynow"
primary="True" primary="True"
> >
<xpath expr="//t[@t-raw='message.body']" position="after"> <xpath expr="//t[@t-out='message.body']" position="after">
<t t-raw="0" /> <t t-out="0" />
<t t-if="record._name == 'contract.contract'"> <t t-if="record._name == 'contract.contract'">
<t <t
t-set="share_url" t-set="share_url"

View File

@@ -1566,7 +1566,7 @@ msgstr ""
#: code:addons/contract/models/contract.py:0 #: code:addons/contract/models/contract.py:0
#, fuzzy, python-format #, fuzzy, python-format
msgid "Please define a %s journal for the company '%s'." msgid "Please define a %s journal for the company '%s'."
msgstr "Molimo definirajte dnevnik prodaje za poduzeće '%s'." msgstr "Molimo definirajte dnevnik prodaje '%s' za poduzeće '%s'."
#. module: contract #. module: contract
#: model:ir.model.fields,field_description:contract.field_contract_contract__access_url #: model:ir.model.fields,field_description:contract.field_contract_contract__access_url

View File

@@ -1581,7 +1581,7 @@ msgstr "Pianificazione successore non permessa per questa riga"
#: code:addons/contract/models/contract.py:0 #: code:addons/contract/models/contract.py:0
#, python-format #, python-format
msgid "Please define a %s journal for the company '%s'." msgid "Please define a %s journal for the company '%s'."
msgstr "Definire un registro vendite per l'azienda \"%s\"." msgstr "Definire un registro vendite %s per l'azienda \"%s\"."
#. module: contract #. module: contract
#: model:ir.model.fields,field_description:contract.field_contract_contract__access_url #: model:ir.model.fields,field_description:contract.field_contract_contract__access_url

View File

@@ -1564,7 +1564,7 @@ msgstr ""
#: code:addons/contract/models/contract.py:0 #: code:addons/contract/models/contract.py:0
#, fuzzy, python-format #, fuzzy, python-format
msgid "Please define a %s journal for the company '%s'." msgid "Please define a %s journal for the company '%s'."
msgstr "Lütfen '%s' şirketi için bir satış yevmiyesi tanımlayın." msgstr "Lütfen '%s' şirketi için bir satış yevmiyesi tanımlayın ('%s')."
#. module: contract #. module: contract
#: model:ir.model.fields,field_description:contract.field_contract_contract__access_url #: model:ir.model.fields,field_description:contract.field_contract_contract__access_url

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
This is a copy / paste from contract/data/mail_template.xml
The only differences are:
- Use noupdate=0
- remove template_contract_modification which remains unchanged between 14 and 15
-->
<odoo>
<record id="email_contract_template" model="mail.template">
<field name="name">Email Contract Template</field>
<field
name="email_from"
>{{ (object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '') }}</field>
<field
name="subject"
>{{ object.company_id.name }} Contract (Ref {{ object.name or 'n/a' }})</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="model_id" ref="model_contract_contract" />
<field name="auto_delete" eval="True" />
<field name="report_template" ref="contract.report_contract" />
<field name="report_name">Contract</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="body_html" type="html">
<div
style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; "
>
<p>Hello <t t-out="object.partner_id.name or '' " />,</p>
<p>A new contract has been created: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&amp;nbsp;&amp;nbsp;<strong>REFERENCES</strong><br />
&amp;nbsp;&amp;nbsp;Contract: <strong t-out="object.name" /><br />
<t t-if="object.date_start">
&amp;nbsp;&amp;nbsp;Contract Date Start: <t
t-out="object.date_start or ''"
/><br />
</t>
<t t-if="object.user_id">
<t t-if="object.user_id.email">
&amp;nbsp;&amp;nbsp;Your Contact: <a
t-att-href="'mailto:%s?subject=Contract %s' % (object.user_id.email, object.name)"
t-out="object.user_id.name"
/>
</t>
<t t-else="">
&amp;nbsp;&amp;nbsp;Your Contact: <t
t-out="object.user_id.name"
/>
</t>
</t>
</p>
<br />
<p>If you have any questions, do not hesitate to contact us.</p>
<p>Thank you for choosing <t
t-out="object.company_id.name or 'us'"
/>!</p>
<br />
<br />
<div
style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;"
>
<h3
style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;"
>
<strong
style="text-transform:uppercase;"
t-out="object.company_id.name"
/></h3>
</div>
<div
style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;"
>
<span style="color: #222; margin-bottom: 5px; display: block; ">
<address
t-field="object.company_id.sudo().partner_id"
t-options='{"widget": "contact", "fields": ["name", "address"], "no_marker": True}'
/>
</span>
<t t-if="object.company_id.phone">
<div
style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; "
>
Phone: <t t-out="object.company_id.phone" />
</div>
</t>
<t t-if="object.company_id.website">
<div>
Web: <a
t-att-href="object.company_id.website"
t-out="object.company_id.website"
/>
</div>
</t>
</div>
<br />
<a
t-att-href="'%s/my/contracts/%s?access_token=%s' % (object.get_base_url(), object.id, object.access_token)"
target="_blank"
style="background-color:#875A7B;padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;"
>View contract</a>
</div>
</field>
</record>
<record id="mail_template_contract_modification" model="mail.template">
<field name="name">Contract Modification Template</field>
<field
name="email_from"
>{{ (object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '') }}</field>
<field
name="subject"
>{{ object.company_id.name }} Contract (Ref {{ object.name or 'n/a' }}) - Modifications</field>
<field name="model_id" ref="model_contract_contract" />
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="body_html" type="html">
<p>Hello</p>
<p>We have modifications on the contract that we want to notify you.</p>
</field>
</record>
<template
id="mail_notification_contract"
inherit_id="mail.mail_notification_paynow"
primary="True"
>
<xpath expr="//t[@t-out='message.body']" position="after">
<t t-out="0" />
<t t-if="record._name == 'contract.contract'">
<t
t-set="share_url"
t-value="record._get_share_url(redirect=True, signup_partner=True, share_token=True)"
/>
<t
t-set="access_url"
t-value="is_online and share_url and base_url + share_url or ''"
/>
</t>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,17 @@
from openupgradelib import openupgrade
@openupgrade.migrate()
def migrate(env, version):
openupgrade.load_data(
env.cr, "contract", "migrations/15.0.1.0.0/noupdate_changes.xml"
)
openupgrade.delete_record_translations(
env.cr,
"contract",
[
"email_contract_template",
"mail_template_contract_modification",
"mail_notification_contract",
],
)

View File

@@ -20,14 +20,22 @@ class ContractAbstractContractLine(models.AbstractModel):
name = fields.Text(string="Description", required=True) name = fields.Text(string="Description", required=True)
quantity = fields.Float(default=1.0, required=True) quantity = fields.Float(default=1.0, required=True)
uom_id = fields.Many2one("uom.uom", string="Unit of Measure") product_uom_category_id = fields.Many2one( # Used for domain of field uom_id
comodel_name="uom.category",
related="product_id.uom_id.category_id",
)
uom_id = fields.Many2one(
comodel_name="uom.uom",
string="Unit of Measure",
domain="[('category_id', '=', product_uom_category_id)]",
)
automatic_price = fields.Boolean( automatic_price = fields.Boolean(
string="Auto-price?", string="Auto-price?",
help="If this is marked, the price will be obtained automatically " help="If this is marked, the price will be obtained automatically "
"applying the pricelist to the product. If not, you will be " "applying the pricelist to the product. If not, you will be "
"able to introduce a manual price", "able to introduce a manual price",
) )
specific_price = fields.Float(string="Specific Price") specific_price = fields.Float()
price_unit = fields.Float( price_unit = fields.Float(
string="Unit Price", string="Unit Price",
compute="_compute_price_unit", compute="_compute_price_unit",
@@ -45,7 +53,6 @@ class ContractAbstractContractLine(models.AbstractModel):
" It should be less or equal to 100", " It should be less or equal to 100",
) )
sequence = fields.Integer( sequence = fields.Integer(
string="Sequence",
default=10, default=10,
help="Sequence of the contract line when displaying contracts", help="Sequence of the contract line when displaying contracts",
) )
@@ -76,7 +83,7 @@ class ContractAbstractContractLine(models.AbstractModel):
readonly=False, readonly=False,
copy=True, copy=True,
) )
last_date_invoiced = fields.Date(string="Last Date Invoiced") last_date_invoiced = fields.Date()
is_canceled = fields.Boolean(string="Canceled", default=False) is_canceled = fields.Boolean(string="Canceled", default=False)
is_auto_renew = fields.Boolean(string="Auto Renew", default=False) is_auto_renew = fields.Boolean(string="Auto Renew", default=False)
auto_renew_interval = fields.Integer( auto_renew_interval = fields.Integer(
@@ -157,6 +164,7 @@ class ContractAbstractContractLine(models.AbstractModel):
def _compute_date_start(self): def _compute_date_start(self):
self._set_recurrence_field("date_start") self._set_recurrence_field("date_start")
# pylint: disable=missing-return
@api.depends("contract_id.recurring_next_date", "contract_id.line_recurrence") @api.depends("contract_id.recurring_next_date", "contract_id.line_recurrence")
def _compute_recurring_next_date(self): def _compute_recurring_next_date(self):
super()._compute_recurring_next_date() super()._compute_recurring_next_date()
@@ -232,13 +240,7 @@ class ContractAbstractContractLine(models.AbstractModel):
@api.onchange("product_id") @api.onchange("product_id")
def _onchange_product_id(self): def _onchange_product_id(self):
if not self.product_id:
return {"domain": {"uom_id": []}}
vals = {} vals = {}
domain = {
"uom_id": [("category_id", "=", self.product_id.uom_id.category_id.id)]
}
if not self.uom_id or ( if not self.uom_id or (
self.product_id.uom_id.category_id.id != self.uom_id.category_id.id self.product_id.uom_id.category_id.id != self.uom_id.category_id.id
): ):
@@ -257,4 +259,3 @@ class ContractAbstractContractLine(models.AbstractModel):
vals["name"] = self.product_id.get_product_multiline_description_sale() vals["name"] = self.product_id.get_product_multiline_description_sale()
vals["price_unit"] = product.price vals["price_unit"] = product.price
self.update(vals) self.update(vals)
return {"domain": domain}

View File

@@ -89,6 +89,7 @@ class ContractContract(models.Model):
string="Invoicing contact", string="Invoicing contact",
comodel_name="res.partner", comodel_name="res.partner",
ondelete="restrict", ondelete="restrict",
domain="['|',('id', 'parent_of', partner_id), ('id', 'child_of', partner_id)]",
) )
partner_id = fields.Many2one( partner_id = fields.Many2one(
comodel_name="res.partner", inverse="_inverse_partner_id", required=True comodel_name="res.partner", inverse="_inverse_partner_id", required=True
@@ -299,7 +300,7 @@ class ContractContract(models.Model):
@api.depends( @api.depends(
"contract_line_ids.recurring_next_date", "contract_line_ids.recurring_next_date",
"contract_line_ids.is_canceled", "contract_line_ids.is_canceled",
) ) # pylint: disable=missing-return
def _compute_recurring_next_date(self): def _compute_recurring_next_date(self):
for contract in self: for contract in self:
recurring_next_date = contract.contract_line_ids.filtered( recurring_next_date = contract.contract_line_ids.filtered(
@@ -371,15 +372,6 @@ class ContractContract(models.Model):
else: else:
self.payment_term_id = partner.property_payment_term_id self.payment_term_id = partner.property_payment_term_id
self.invoice_partner_id = self.partner_id.address_get(["invoice"])["invoice"] self.invoice_partner_id = self.partner_id.address_get(["invoice"])["invoice"]
return {
"domain": {
"invoice_partner_id": [
"|",
("id", "parent_of", self.partner_id.id),
("id", "child_of", self.partner_id.id),
]
}
}
def _convert_contract_lines(self, contract): def _convert_contract_lines(self, contract):
self.ensure_one() self.ensure_one()
@@ -416,8 +408,14 @@ class ContractContract(models.Model):
) )
if not journal: if not journal:
raise ValidationError( raise ValidationError(
_("Please define a %s journal for the company '%s'.") _(
% (self.contract_type, self.company_id.name or "") "Please define a %(contract_type)s journal "
"for the company '%(company)s'."
)
% {
"contract_type": self.contract_type,
"company": self.company_id.name or "",
}
) )
invoice_type = "out_invoice" invoice_type = "out_invoice"
if self.contract_type == "purchase": if self.contract_type == "purchase":
@@ -566,10 +564,16 @@ class ContractContract(models.Model):
self.message_post( self.message_post(
body=_( body=_(
"Contract manually invoiced: " "Contract manually invoiced: "
'<a href="#" data-oe-model="%s" data-oe-id="%s">Invoice' "<a"
' href="#" data-oe-model="%(model_name)s" '
' data-oe-id="%(rec_id)s"'
">Invoice"
"</a>" "</a>"
) )
% (invoice._name, invoice.id) % {
"model_name": invoice._name,
"rec_id": invoice.id,
}
) )
return invoice return invoice

View File

@@ -22,9 +22,7 @@ class ContractLine(models.Model):
] ]
_order = "sequence,id" _order = "sequence,id"
sequence = fields.Integer( sequence = fields.Integer()
string="Sequence",
)
contract_id = fields.Many2one( contract_id = fields.Many2one(
comodel_name="contract.contract", comodel_name="contract.contract",
string="Contract", string="Contract",
@@ -44,7 +42,6 @@ class ContractLine(models.Model):
date_start = fields.Date(required=True) date_start = fields.Date(required=True)
date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False) date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False)
termination_notice_date = fields.Date( termination_notice_date = fields.Date(
string="Termination notice date",
compute="_compute_termination_notice_date", compute="_compute_termination_notice_date",
store=True, store=True,
copy=False, copy=False,
@@ -72,7 +69,6 @@ class ContractLine(models.Model):
help="Contract Line origin of this one.", help="Contract Line origin of this one.",
) )
manual_renew_needed = fields.Boolean( manual_renew_needed = fields.Boolean(
string="Manual renew needed",
default=False, default=False,
help="This flag is used to make a difference between a definitive stop" help="This flag is used to make a difference between a definitive stop"
"and temporary one for which a user is not able to plan a" "and temporary one for which a user is not able to plan a"
@@ -92,7 +88,6 @@ class ContractLine(models.Model):
string="Un-Cancel allowed?", compute="_compute_allowed" string="Un-Cancel allowed?", compute="_compute_allowed"
) )
state = fields.Selection( state = fields.Selection(
string="State",
selection=[ selection=[
("upcoming", "Upcoming"), ("upcoming", "Upcoming"),
("in-progress", "In-progress"), ("in-progress", "In-progress"),
@@ -109,12 +104,11 @@ class ContractLine(models.Model):
related="contract_id.active", related="contract_id.active",
store=True, store=True,
readonly=True, readonly=True,
default=True,
) )
@api.depends( @api.depends(
"last_date_invoiced", "date_start", "date_end", "contract_id.last_date_invoiced" "last_date_invoiced", "date_start", "date_end", "contract_id.last_date_invoiced"
) ) # pylint: disable=missing-return
def _compute_next_period_date_start(self): def _compute_next_period_date_start(self):
"""Rectify next period date start if another line in the contract has been """Rectify next period date start if another line in the contract has been
already invoiced previously when the recurrence is by contract. already invoiced previously when the recurrence is by contract.
@@ -696,15 +690,15 @@ class ContractLine(models.Model):
) )
if post_message: if post_message:
msg = _( msg = _(
"""Contract line for <strong>{product}</strong> """Contract line for <strong>%(product)s</strong>
stopped: <br/> stopped: <br/>
- <strong>End</strong>: {old_end} -- {new_end} - <strong>End</strong>: %(old_end)s -- %(new_end)s
""".format( """
product=rec.name, ) % {
old_end=old_date_end, "product": rec.name,
new_end=rec.date_end, "old_end": old_date_end,
) "new_end": rec.date_end,
) }
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
else: else:
rec.write( rec.write(
@@ -770,17 +764,17 @@ class ContractLine(models.Model):
contract_line |= new_line contract_line |= new_line
if post_message: if post_message:
msg = _( msg = _(
"""Contract line for <strong>{product}</strong> """Contract line for <strong>%(product)s</strong>
planned a successor: <br/> planned a successor: <br/>
- <strong>Start</strong>: {new_date_start} - <strong>Start</strong>: %(new_date_start)s
<br/> <br/>
- <strong>End</strong>: {new_date_end} - <strong>End</strong>: %(new_date_end)s
""".format( """
product=rec.name, ) % {
new_date_start=new_line.date_start, "product": rec.name,
new_date_end=new_line.date_end, "new_date_start": new_line.date_start,
) "new_date_end": new_line.date_end,
) }
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
return contract_line return contract_line
@@ -873,17 +867,17 @@ class ContractLine(models.Model):
post_message=False, post_message=False,
) )
msg = _( msg = _(
"""Contract line for <strong>{product}</strong> """Contract line for <strong>%(product)s</strong>
suspended: <br/> suspended: <br/>
- <strong>Suspension Start</strong>: {new_date_start} - <strong>Suspension Start</strong>: %(new_date_start)s
<br/> <br/>
- <strong>Suspension End</strong>: {new_date_end} - <strong>Suspension End</strong>: %(new_date_end)s
""".format( """
product=rec.name, ) % {
new_date_start=date_start, "product": rec.name,
new_date_end=date_end, "new_date_start": date_start,
) "new_date_end": date_end,
) }
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
return contract_line return contract_line
@@ -893,10 +887,13 @@ class ContractLine(models.Model):
for contract in self.mapped("contract_id"): for contract in self.mapped("contract_id"):
lines = self.filtered(lambda l, c=contract: l.contract_id == c) lines = self.filtered(lambda l, c=contract: l.contract_id == c)
msg = _( msg = _(
"""Contract line canceled: %s""" "Contract line canceled: %s",
% "<br/>- ".join( "<br/>- ".join(
["<strong>%s</strong>" % name for name in lines.mapped("name")] [
) "<strong>%(product)s</strong>" % {"product": name}
for name in lines.mapped("name")
]
),
) )
contract.message_post(body=msg) contract.message_post(body=msg)
self.mapped("predecessor_contract_line_id").write( self.mapped("predecessor_contract_line_id").write(
@@ -910,10 +907,13 @@ class ContractLine(models.Model):
for contract in self.mapped("contract_id"): for contract in self.mapped("contract_id"):
lines = self.filtered(lambda l, c=contract: l.contract_id == c) lines = self.filtered(lambda l, c=contract: l.contract_id == c)
msg = _( msg = _(
"""Contract line Un-canceled: %s""" "Contract line Un-canceled: %s",
% "<br/>- ".join( "<br/>- ".join(
["<strong>%s</strong>" % name for name in lines.mapped("name")] [
) "<strong>%(product)s</strong>" % {"product": name}
for name in lines.mapped("name")
]
),
) )
contract.message_post(body=msg) contract.message_post(body=msg)
for rec in self: for rec in self:
@@ -1036,17 +1036,17 @@ class ContractLine(models.Model):
new_line = rec._renew_extend_line(date_end) new_line = rec._renew_extend_line(date_end)
res |= new_line res |= new_line
msg = _( msg = _(
"""Contract line for <strong>{product}</strong> """Contract line for <strong>%(product)s</strong>
renewed: <br/> renewed: <br/>
- <strong>Start</strong>: {new_date_start} - <strong>Start</strong>: %(new_date_start)s
<br/> <br/>
- <strong>End</strong>: {new_date_end} - <strong>End</strong>: %(new_date_end)s
""".format( """
product=rec.name, ) % {
new_date_start=date_start, "product": rec.name,
new_date_end=date_end, "new_date_start": date_start,
) "new_date_end": date_end,
) }
rec.contract_id.message_post(body=msg) rec.contract_id.message_post(body=msg)
return res return res

View File

@@ -10,8 +10,8 @@ class ContractModification(models.Model):
_description = "Contract Modification" _description = "Contract Modification"
_order = "date desc" _order = "date desc"
date = fields.Date(required=True, string="Date") date = fields.Date(required=True)
description = fields.Text(required=True, string="Description") description = fields.Text(required=True)
contract_id = fields.Many2one( contract_id = fields.Many2one(
string="Contract", string="Contract",
comodel_name="contract.contract", comodel_name="contract.contract",
@@ -19,10 +19,7 @@ class ContractModification(models.Model):
ondelete="cascade", ondelete="cascade",
index=True, index=True,
) )
sent = fields.Boolean( sent = fields.Boolean(default=False)
string="Sent",
default=False,
)
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):

View File

@@ -47,7 +47,7 @@ class ContractRecurrencyBasicMixin(models.AbstractModel):
string="Invoice Every", string="Invoice Every",
help="Invoice every (Days/Week/Month/Year)", help="Invoice every (Days/Week/Month/Year)",
) )
date_start = fields.Date(string="Date Start") date_start = fields.Date()
recurring_next_date = fields.Date(string="Date of Next Invoice") recurring_next_date = fields.Date(string="Date of Next Invoice")
@api.depends("recurring_invoicing_type", "recurring_rule_type") @api.depends("recurring_invoicing_type", "recurring_rule_type")
@@ -80,7 +80,7 @@ class ContractRecurrencyMixin(models.AbstractModel):
recurring_next_date = fields.Date( recurring_next_date = fields.Date(
compute="_compute_recurring_next_date", store=True, readonly=False, copy=True compute="_compute_recurring_next_date", store=True, readonly=False, copy=True
) )
date_end = fields.Date(string="Date End", index=True) date_end = fields.Date(index=True)
next_period_date_start = fields.Date( next_period_date_start = fields.Date(
string="Next Period Start", string="Next Period Start",
compute="_compute_next_period_date_start", compute="_compute_next_period_date_start",
@@ -89,9 +89,7 @@ class ContractRecurrencyMixin(models.AbstractModel):
string="Next Period End", string="Next Period End",
compute="_compute_next_period_date_end", compute="_compute_next_period_date_end",
) )
last_date_invoiced = fields.Date( last_date_invoiced = fields.Date(readonly=True, copy=False)
string="Last Date Invoiced", readonly=True, copy=False
)
@api.depends("next_period_date_start") @api.depends("next_period_date_start")
def _compute_recurring_next_date(self): def _compute_recurring_next_date(self):

View File

@@ -9,7 +9,6 @@ class ResCompany(models.Model):
_inherit = "res.company" _inherit = "res.company"
create_new_line_at_contract_line_renew = fields.Boolean( create_new_line_at_contract_line_renew = fields.Boolean(
string="Create New Line At Contract Line Renew",
help="If checked, a new line will be generated at contract line renew " help="If checked, a new line will be generated at contract line renew "
"and linked to the original one as successor. The default " "and linked to the original one as successor. The default "
"behavior is to extend the end date of the contract by a new " "behavior is to extend the end date of the contract by a new "

View File

@@ -3,6 +3,7 @@
# Copyright 2021 Tecnativa - Víctor Martínez # Copyright 2021 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from collections import namedtuple from collections import namedtuple
from datetime import timedelta from datetime import timedelta
@@ -17,7 +18,7 @@ def to_date(date):
return fields.Date.to_date(date) return fields.Date.to_date(date)
class TestContractBase(common.SavepointCase): class TestContractBase(common.TransactionCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
@@ -252,8 +253,6 @@ class TestContract(TestContractBase):
def test_contract(self): def test_contract(self):
self.assertEqual(self.contract.recurring_next_date, to_date("2018-01-15")) self.assertEqual(self.contract.recurring_next_date, to_date("2018-01-15"))
self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0)
res = self.acct_line._onchange_product_id()
self.assertIn("uom_id", res["domain"])
self.acct_line.price_unit = 100.0 self.acct_line.price_unit = 100.0
self.contract.partner_id = self.partner.id self.contract.partner_id = self.partner.id
self.contract.recurring_create_invoice() self.contract.recurring_create_invoice()
@@ -489,11 +488,6 @@ class TestContract(TestContractBase):
self.acct_line._onchange_product_id() self.acct_line._onchange_product_id()
self.assertEqual(self.acct_line.uom_id, self.acct_line.product_id.uom_id) self.assertEqual(self.acct_line.uom_id, self.acct_line.product_id.uom_id)
def test_onchange_product_id(self):
line = self.env["contract.line"].new()
res = line._onchange_product_id()
self.assertFalse(res["domain"]["uom_id"])
def test_no_pricelist(self): def test_no_pricelist(self):
self.contract.pricelist_id = False self.contract.pricelist_id = False
self.acct_line.quantity = 2 self.acct_line.quantity = 2
@@ -565,8 +559,15 @@ class TestContract(TestContractBase):
test_value = contract_line[key] test_value = contract_line[key]
try: try:
test_value = test_value.id test_value = test_value.id
except AttributeError: except AttributeError as ae:
pass # This try/except is for relation fields.
# For normal fields, test_value would be
# str, float, int ... without id
logging.info(
"Ignored AttributeError ('%s' is not a relation field): %s",
key,
ae,
)
self.assertEqual(test_value, value) self.assertEqual(test_value, value)
def test_send_mail_contract(self): def test_send_mail_contract(self):
@@ -581,21 +582,6 @@ class TestContract(TestContractBase):
self.contract._onchange_contract_type() self.contract._onchange_contract_type()
self.assertFalse(any(self.contract.contract_line_ids.mapped("automatic_price"))) self.assertFalse(any(self.contract.contract_line_ids.mapped("automatic_price")))
def test_contract_onchange_product_id_domain_blank(self):
"""It should return a blank UoM domain when no product."""
line = self.env["contract.template.line"].new()
res = line._onchange_product_id()
self.assertFalse(res["domain"]["uom_id"])
def test_contract_onchange_product_id_domain(self):
"""It should return UoM category domain."""
line = self._add_template_line()
res = line._onchange_product_id()
self.assertEqual(
res["domain"]["uom_id"][0],
("category_id", "=", self.product_1.uom_id.category_id.id),
)
def test_contract_onchange_product_id_uom(self): def test_contract_onchange_product_id_uom(self):
"""It should update the UoM for the line.""" """It should update the UoM for the line."""
line = self._add_template_line( line = self._add_template_line(
@@ -647,14 +633,17 @@ class TestContract(TestContractBase):
show_contract = self.partner.with_context( show_contract = self.partner.with_context(
contract_type="sale" contract_type="sale"
).act_show_contract() ).act_show_contract()
self.assertDictContainsSubset( self.assertEqual(
show_contract,
{ {
**show_contract,
**{
"name": "Customer Contracts", "name": "Customer Contracts",
"type": "ir.actions.act_window", "type": "ir.actions.act_window",
"res_model": "contract.contract", "res_model": "contract.contract",
"xml_id": "contract.action_customer_contract", "xml_id": "contract.action_customer_contract",
}, },
show_contract, },
"There was an error and the view couldn't be opened.", "There was an error and the view couldn't be opened.",
) )
@@ -2309,7 +2298,7 @@ class TestContract(TestContractBase):
action = self.contract.action_terminate_contract() action = self.contract.action_terminate_contract()
wizard = ( wizard = (
self.env[action["res_model"]] self.env[action["res_model"]]
.with_context(action["context"]) .with_context(**action["context"])
.create( .create(
{ {
"terminate_date": "2018-03-01", "terminate_date": "2018-03-01",

View File

@@ -21,6 +21,8 @@
name="product_id" name="product_id"
attrs="{'required': [('display_type', '=', False)]}" attrs="{'required': [('display_type', '=', False)]}"
/> />
<field name="product_uom_category_id" invisible="1" />
<label for="quantity" /> <label for="quantity" />
<div class="o_row"> <div class="o_row">
<field name="quantity" class="oe_inline" /> <field name="quantity" class="oe_inline" />

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Tecnativa - Ernesto Tejeda
Copyright 2020 Tecnativa - Víctor Martínez
Copyright 2020 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<template
id="assets_backend"
name="contract assets"
inherit_id="web.assets_backend"
>
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/contract/static/src/js/section_and_note_fields_backend.js"
/>
</xpath>
</template>
<template
id="assets_frontend"
inherit_id="web.assets_frontend"
name="contract assets"
>
<xpath expr="." position="inside">
<link
rel="stylesheet"
href="/contract/static/src/scss/frontend.scss"
type="text/css"
/>
</xpath>
</template>
<template id="assets_tests" inherit_id="web.assets_tests">
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/contract/static/src/js/contract_portal_tour.js"
/>
</xpath>
</template>
</odoo>

View File

@@ -202,6 +202,10 @@
<field name="display_type" invisible="1" /> <field name="display_type" invisible="1" />
<field name="sequence" widget="handle" /> <field name="sequence" widget="handle" />
<field name="product_id" /> <field name="product_id" />
<field
name="product_uom_category_id"
invisible="1"
/>
<field name="name" widget="section_and_note_text" /> <field name="name" widget="section_and_note_text" />
<field <field
name="analytic_account_id" name="analytic_account_id"

View File

@@ -9,12 +9,11 @@ class ContractLineWizard(models.TransientModel):
_name = "contract.line.wizard" _name = "contract.line.wizard"
_description = "Contract Line Wizard" _description = "Contract Line Wizard"
date_start = fields.Date(string="Date Start") date_start = fields.Date()
date_end = fields.Date(string="Date End") date_end = fields.Date()
recurring_next_date = fields.Date(string="Next Invoice Date") recurring_next_date = fields.Date(string="Next Invoice Date")
is_auto_renew = fields.Boolean(string="Auto Renew", default=False) is_auto_renew = fields.Boolean(default=False)
manual_renew_needed = fields.Boolean( manual_renew_needed = fields.Boolean(
string="Manual renew needed",
default=False, default=False,
help="This flag is used to make a difference between a definitive stop" help="This flag is used to make a difference between a definitive stop"
"and temporary one for which a user is not able to plan a" "and temporary one for which a user is not able to plan a"

View File

@@ -9,7 +9,7 @@ class ContractManuallyCreateInvoice(models.TransientModel):
_name = "contract.manually.create.invoice" _name = "contract.manually.create.invoice"
_description = "Contract Manually Create Invoice Wizard" _description = "Contract Manually Create Invoice Wizard"
invoice_date = fields.Date(string="Invoice Date", required=True) invoice_date = fields.Date(required=True)
contract_to_invoice_count = fields.Integer( contract_to_invoice_count = fields.Integer(
compute="_compute_contract_to_invoice_ids" compute="_compute_contract_to_invoice_ids"
) )