[MERGE] forward port branch 11.0 up to fe22a0f9ca
This commit is contained in:
@@ -346,7 +346,7 @@ class AccountBankStatement(models.Model):
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_name = "account.bank.statement.line"
|
||||
_description = "Bank Statement Line"
|
||||
_order = "statement_id desc, sequence, id desc"
|
||||
_order = "statement_id desc, date desc, sequence, id desc"
|
||||
|
||||
name = fields.Char(string='Label', required=True)
|
||||
date = fields.Date(required=True, default=lambda self: self._context.get('date', fields.Date.context_today(self)))
|
||||
|
||||
@@ -951,7 +951,7 @@ class AccountInvoice(models.Model):
|
||||
total_currency = 0
|
||||
for line in invoice_move_lines:
|
||||
if self.currency_id != company_currency:
|
||||
currency = self.currency_id.with_context(date=self.date or self.date_invoice or fields.Date.context_today(self))
|
||||
currency = self.currency_id.with_context(date=self._get_currency_rate_date() or fields.Date.context_today(self))
|
||||
if not (line.get('currency_id') and line.get('amount_currency')):
|
||||
line['currency_id'] = currency.id
|
||||
line['amount_currency'] = currency.round(line['price'])
|
||||
@@ -1101,7 +1101,7 @@ class AccountInvoice(models.Model):
|
||||
if inv.payment_term_id:
|
||||
totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=company_currency.id).compute(total, inv.date_invoice)[0]
|
||||
res_amount_currency = total_currency
|
||||
ctx['date'] = inv.date or inv.date_invoice
|
||||
ctx['date'] = inv._get_currency_rate_date()
|
||||
for i, t in enumerate(totlines):
|
||||
if inv.currency_id != company_currency:
|
||||
amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
|
||||
@@ -1300,6 +1300,9 @@ class AccountInvoice(models.Model):
|
||||
copy_fields = ['company_id', 'user_id', 'fiscal_position_id']
|
||||
return self._get_refund_common_fields() + self._get_refund_prepare_fields() + copy_fields
|
||||
|
||||
def _get_currency_rate_date(self):
|
||||
return self.date or self.date_invoice
|
||||
|
||||
@api.model
|
||||
def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None):
|
||||
""" Prepare the dict of values to create the new credit note from the invoice.
|
||||
@@ -1460,7 +1463,7 @@ class AccountInvoiceLine(models.Model):
|
||||
@api.one
|
||||
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
|
||||
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
|
||||
'invoice_id.date_invoice')
|
||||
'invoice_id.date_invoice', 'invoice_id.date')
|
||||
def _compute_price(self):
|
||||
currency = self.invoice_id and self.invoice_id.currency_id or None
|
||||
price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
|
||||
@@ -1470,7 +1473,7 @@ class AccountInvoiceLine(models.Model):
|
||||
self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price
|
||||
self.price_total = taxes['total_included'] if taxes else self.price_subtotal
|
||||
if self.invoice_id.currency_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id:
|
||||
price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id.date_invoice).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id)
|
||||
price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id._get_currency_rate_date()).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id)
|
||||
sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1
|
||||
self.price_subtotal_signed = price_subtotal_signed * sign
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class AccountMove(models.Model):
|
||||
tax_cash_basis_rec_id = fields.Many2one(
|
||||
'account.partial.reconcile',
|
||||
string='Tax Cash Basis Entry of',
|
||||
help="Technical field used to keep track of the tax cash basis reconciliation."
|
||||
help="Technical field used to keep track of the tax cash basis reconciliation. "
|
||||
"This is needed when cancelling the source: it will post the inverse journal entry to cancel that part too.")
|
||||
auto_reverse = fields.Boolean(string='Reverse Automatically', default=False, help='If this checkbox is ticked, this entry will be automatically reversed at the reversal date you defined.')
|
||||
reverse_date = fields.Date(string='Reversal Date', help='Date of the reverse accounting entry.')
|
||||
@@ -174,6 +174,8 @@ class AccountMove(models.Model):
|
||||
if not move.journal_id.update_posted:
|
||||
raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.'))
|
||||
if self.ids:
|
||||
self.check_access_rights('write')
|
||||
self.check_access_rule('write')
|
||||
self._check_lock_date()
|
||||
self._cr.execute('UPDATE account_move '\
|
||||
'SET state=%s '\
|
||||
@@ -544,9 +546,9 @@ class AccountMoveLine(models.Model):
|
||||
for line in self:
|
||||
amount = line.amount_currency
|
||||
if line.currency_id and line.currency_id != line.company_currency_id:
|
||||
amount = self.currency_id.with_context(date=line.date).compute(amount, line.company_currency_id)
|
||||
line.debit = amount > 0 and amount or 0.0
|
||||
line.credit = amount < 0 and -amount or 0.0
|
||||
amount = line.currency_id.with_context(date=line.date).compute(amount, line.company_currency_id)
|
||||
line.debit = amount > 0 and amount or 0.0
|
||||
line.credit = amount < 0 and -amount or 0.0
|
||||
|
||||
####################################################
|
||||
# Reconciliation interface methods
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<report
|
||||
id="action_report_journal"
|
||||
model="account.common.journal.report"
|
||||
string="Sale/Purchase Journal"
|
||||
string="Journals Audit"
|
||||
report_type="qweb-pdf"
|
||||
name="account.report_journal"
|
||||
file="account.report_journal"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</p>
|
||||
|
||||
<t t-if="Lines[o.id]">
|
||||
<span t-field="res_company.overdue_msg"/>
|
||||
<span t-field="res_company.with_context(lang=o.lang).overdue_msg"/>
|
||||
<table class="table table-condensed" t-foreach="Lines[o.id]" t-as="currency">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
|
||||
<record id="account_report_print_journal_view" model="ir.ui.view">
|
||||
<field name="name">Sale/Purchase Journal</field>
|
||||
<field name="name">Journals Audit</field>
|
||||
<field name="model">account.print.journal</field>
|
||||
<field name="inherit_id" ref="account.account_common_report_view"/>
|
||||
<field name="arch" type="xml">
|
||||
@@ -12,15 +12,12 @@
|
||||
<field name="sort_selection" widget="radio"/>
|
||||
<newline/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='journal_ids']" position="attributes">
|
||||
<attribute name="domain">[('type', 'in', ('sale', 'purchase'))]</attribute>
|
||||
</xpath>
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_print_journal_menu" model="ir.actions.act_window">
|
||||
<field name="name">Sale/Purchase Journal</field>
|
||||
<field name="name">Journals Audit</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.print.journal</field>
|
||||
<field name="view_type">form</field>
|
||||
@@ -31,7 +28,7 @@
|
||||
|
||||
<menuitem
|
||||
id="menu_print_journal"
|
||||
name="Sale/Purchase Journal"
|
||||
name="Journals Audit"
|
||||
parent="account.menu_finance_legal_statement"
|
||||
action="action_account_print_journal_menu"
|
||||
groups="account.group_account_manager,account.group_account_user"
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<field name="crossovered_budget_line" context="{'default_date_from': date_from,'default_date_to': date_to}" colspan="4" nolabel="1" attrs="{'readonly':[('state','!=','draft')]}">
|
||||
<tree string="Budget Lines">
|
||||
<field name="general_budget_id"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" required="1"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="paid_date" groups="base.group_no_one"/>
|
||||
@@ -111,7 +111,7 @@
|
||||
<group>
|
||||
<field name="general_budget_id"/>
|
||||
<field name="planned_amount" widget="monetary"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" required="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="date_from" string="Period"/>
|
||||
|
||||
@@ -19,11 +19,7 @@
|
||||
<field name="auth_signup_uninvited" class="o_light_label" widget="radio" required="True"/>
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('auth_signup_uninvited','=','b2b')]}">
|
||||
<div class="mt16" groups="base.group_no_one"
|
||||
attrs="{'invisible': [('auth_signup_template_user_id', '!=', False)]}">
|
||||
Copy access rights from <field name="auth_signup_template_user_id"/>
|
||||
</div>
|
||||
<div class="mt16" attrs="{'invisible': [('auth_signup_template_user_id', '=', False)]}">
|
||||
<div class="mt16">
|
||||
<button type="object" name="open_template_user" string="Default Access Rights" icon="fa-arrow-right" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,11 +164,12 @@ QUnit.test('do no update form twice after a command barcode scanned', function (
|
||||
|
||||
var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms;
|
||||
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0;
|
||||
var formUpdate = FormController.prototype.update;
|
||||
FormController.prototype.update = function () {
|
||||
assert.step('update');
|
||||
return formUpdate.apply(this, arguments);
|
||||
};
|
||||
testUtils.patch(FormController, {
|
||||
update: function () {
|
||||
assert.step('update');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -206,7 +207,7 @@ QUnit.test('do no update form twice after a command barcode scanned', function (
|
||||
|
||||
form.destroy();
|
||||
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
|
||||
FormController.prototype.update = formUpdate;
|
||||
testUtils.unpatch(FormController);
|
||||
});
|
||||
|
||||
QUnit.test('widget field_float_scannable', function (assert) {
|
||||
|
||||
@@ -9,7 +9,7 @@ class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
country_enforce_cities = fields.Boolean(related='country_id.enforce_cities')
|
||||
city_id = fields.Many2one('res.city', string='Company')
|
||||
city_id = fields.Many2one('res.city', string='City')
|
||||
|
||||
@api.onchange('city_id')
|
||||
def _onchange_city_id(self):
|
||||
|
||||
@@ -578,7 +578,7 @@ class Import(models.TransientModel):
|
||||
# Check that currency exists
|
||||
currency = self.env['res.currency'].search([('symbol', '=', split_value[currency_index].strip())])
|
||||
if len(currency):
|
||||
return split_value[currency_index + 1 % 2] if not negative else '-' + split_value[currency_index + 1 % 2]
|
||||
return split_value[(currency_index + 1) % 2] if not negative else '-' + split_value[(currency_index + 1) % 2]
|
||||
# Otherwise it is not a float with a currency symbol
|
||||
return False
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<field name="use_leads">True</field>
|
||||
<field name="alias_name">sales</field>
|
||||
<field name="dashboard_graph_model">crm.opportunity.report</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!--default alias for leads-->
|
||||
|
||||
@@ -988,7 +988,7 @@ class Lead(models.Model):
|
||||
result['activity']['today'] += 1
|
||||
if date.today() <= date_deadline <= date.today() + timedelta(days=7):
|
||||
result['activity']['next_7_days'] += 1
|
||||
if date_deadline < date.today() and not opp.date_closed:
|
||||
if date_deadline < date.today():
|
||||
result['activity']['overdue'] += 1
|
||||
# Won in Opportunities
|
||||
if opp.date_closed and opp.stage_id.probability == 100:
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
<record id="crm_rule_lead_assigned_activities" model="ir.rule">
|
||||
<field name="name">Leads with assigned activities</field>
|
||||
<field ref="model_crm_lead" name="model_id"/>
|
||||
<field name="domain_force">[('activity_ids.user_id','=',user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
<record id="crm_rule_all_lead" model="ir.rule">
|
||||
<field name="name">All Leads</field>
|
||||
<field ref="model_crm_lead" name="model_id"/>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<form string="Leads Form">
|
||||
<header>
|
||||
<button name="%(crm.action_crm_lead2opportunity_partner)d" string="Convert to Opportunity" type="action"
|
||||
help="Convert to Opportunity" class="oe_highlight"/>
|
||||
help="Convert to Opportunity" class="oe_highlight" attrs="{'invisible': [('type', '=', 'opportunity')]}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
@@ -385,7 +385,7 @@
|
||||
domain="[('activity_ids.user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d')), ('date_closed', '=', False)]"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
@@ -630,7 +630,7 @@
|
||||
domain="[('activity_ids.user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d')), ('date_closed', '=', False)]"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all opportunities for which the next action date is before today"/>
|
||||
<filter string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
@@ -748,6 +748,34 @@ if record:
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
'Mark Late Activities as Done' in action dropdown
|
||||
-->
|
||||
<record id="action_mark_late_activities_done" model="ir.actions.server">
|
||||
<field name="name">Mark Late Activities as Done</field>
|
||||
<field name="model_id" ref="model_crm_lead"/>
|
||||
<field name="binding_model_id" ref="crm.model_crm_lead"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
for rec in records:
|
||||
rec.activity_ids.filtered(lambda a: a.state == 'overdue').action_feedback(feedback='Closed automatically in batch')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
'Mark All Activities as Done' in action dropdown
|
||||
-->
|
||||
<record id="action_mark_activities_done" model="ir.actions.server">
|
||||
<field name="name">Mark All Activities as Done</field>
|
||||
<field name="model_id" ref="model_crm_lead"/>
|
||||
<field name="binding_model_id" ref="crm.model_crm_lead"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
for rec in records:
|
||||
rec.activity_ids.action_feedback(feedback='Closed automatically in batch')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
Menu Actions
|
||||
-->
|
||||
|
||||
@@ -44,7 +44,7 @@ class EventType(models.Model):
|
||||
help="It will select this default maximum value when you choose this event")
|
||||
auto_confirm = fields.Boolean(
|
||||
'Automatically Confirm Registrations', default=True,
|
||||
help="Events and registrations will automatically be confirmed"
|
||||
help="Events and registrations will automatically be confirmed "
|
||||
"upon creation, easing the flow for simple events.")
|
||||
# location
|
||||
is_online = fields.Boolean(
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
please give details about the registrations</p>
|
||||
<field name="event_registration_ids">
|
||||
<tree string="Registration" editable="top" create="false" delete="false">
|
||||
<field name="event_id" readonly='1'/>
|
||||
<field name="event_id" readonly='1' force_save="1"/>
|
||||
<field name="registration_id" readonly='1'/>
|
||||
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/>
|
||||
<field name="name"/>
|
||||
|
||||
@@ -21,7 +21,7 @@ class FleetVehicle(models.Model):
|
||||
company_id = fields.Many2one('res.company', 'Company')
|
||||
license_plate = fields.Char(required=True, help='License plate number of the vehicle (i = plate number for a car)')
|
||||
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
|
||||
driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle')
|
||||
driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle', copy=False)
|
||||
model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle')
|
||||
log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
|
||||
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
|
||||
|
||||
@@ -117,7 +117,7 @@ class FleetVehicleLogContract(models.Model):
|
||||
copy=False)
|
||||
notes = fields.Text('Terms and Conditions', help='Write here all supplementary information relative to this contract', copy=False)
|
||||
cost_generated = fields.Float('Recurring Cost Amount',
|
||||
help="Costs paid at regular intervals, depending on the cost frequency."
|
||||
help="Costs paid at regular intervals, depending on the cost frequency. "
|
||||
"If the cost frequency is set to unique, the cost will be logged at the start date")
|
||||
cost_frequency = fields.Selection([
|
||||
('no', 'No'),
|
||||
|
||||
@@ -144,7 +144,7 @@ class GoogleService(models.TransientModel):
|
||||
if error.response.status_code == 400: # invalid grant
|
||||
with registry(request.session.db).cursor() as cur:
|
||||
self.env(cur)['res.users'].browse(self.env.uid).write({'google_%s_rtoken' % service: False})
|
||||
error_key = json.loads(error.response.json()).get("error", "nc")
|
||||
error_key = error.response.json().get("error", "nc")
|
||||
_logger.exception("Bad google request : %s !", error_key)
|
||||
error_msg = _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired [%s]") % error_key
|
||||
raise self.env['res.config.settings'].get_config_warning(error_msg)
|
||||
|
||||
@@ -16,10 +16,10 @@ var KioskMode = Widget.extend({
|
||||
var self = this;
|
||||
core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
self.session = Session;
|
||||
this._rpc({
|
||||
var def = this._rpc({
|
||||
model: 'res.company',
|
||||
method: 'search_read',
|
||||
args: [[['id', '=', self.session.company_id]], ['name']],
|
||||
args: [[['id', '=', this.session.company_id]], ['name']],
|
||||
})
|
||||
.then(function (companies){
|
||||
self.company_name = companies[0].name;
|
||||
@@ -27,7 +27,7 @@ var KioskMode = Widget.extend({
|
||||
self.$el.html(QWeb.render("HrAttendanceKioskMode", {widget: self}));
|
||||
self.start_clock();
|
||||
});
|
||||
return self._super.apply(this, arguments);
|
||||
return $.when(def, this._super.apply(this, arguments));
|
||||
},
|
||||
|
||||
_onBarcodeScanned: function(barcode) {
|
||||
|
||||
@@ -19,10 +19,10 @@ var MyAttendances = Widget.extend({
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this._rpc({
|
||||
var def = this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'search_read',
|
||||
args: [[['user_id', '=', self.getSession().uid]], ['attendance_state', 'name']],
|
||||
args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name']],
|
||||
})
|
||||
.then(function (res) {
|
||||
if (_.isEmpty(res) ) {
|
||||
@@ -33,7 +33,7 @@ var MyAttendances = Widget.extend({
|
||||
self.$el.html(QWeb.render("HrAttendanceMyMainMenu", {widget: self}));
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
return $.when(def, this._super.apply(this, arguments));
|
||||
},
|
||||
|
||||
update_attendance: function () {
|
||||
|
||||
@@ -15,6 +15,8 @@ class AccountMoveLine(models.Model):
|
||||
res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id)
|
||||
account_move_ids = [l.move_id.id for l in self if float_compare(l.move_id.matched_percentage, 1, precision_digits=5) == 0]
|
||||
if account_move_ids:
|
||||
expense_sheets = self.env['hr.expense.sheet'].search([('account_move_id', 'in', account_move_ids)])
|
||||
expense_sheets = self.env['hr.expense.sheet'].search([
|
||||
('account_move_id', 'in', account_move_ids), ('state', '!=', 'done')
|
||||
])
|
||||
expense_sheets.set_to_paid()
|
||||
return res
|
||||
|
||||
@@ -445,18 +445,20 @@ class HrExpenseSheet(models.Model):
|
||||
|
||||
@api.multi
|
||||
def check_consistency(self):
|
||||
if any(sheet.employee_id != self[0].employee_id for sheet in self):
|
||||
raise UserError(_("Expenses must belong to the same Employee."))
|
||||
|
||||
expense_lines = self.mapped('expense_line_ids')
|
||||
if expense_lines and any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
|
||||
raise UserError(_("Expenses must have been paid by the same entity (Company or employee)"))
|
||||
for rec in self:
|
||||
expense_lines = rec.expense_line_ids
|
||||
if not expense_lines:
|
||||
continue
|
||||
if any(expense.employee_id != rec.employee_id for expense in expense_lines):
|
||||
raise UserError(_("Expenses must belong to the same Employee."))
|
||||
if any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
|
||||
raise UserError(_("Expenses must have been paid by the same entity (Company or employee)"))
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
self._create_set_followers(vals)
|
||||
sheet = super(HrExpenseSheet, self).create(vals)
|
||||
self.check_consistency()
|
||||
sheet.check_consistency()
|
||||
return sheet
|
||||
|
||||
@api.multi
|
||||
@@ -597,7 +599,7 @@ class HrExpenseSheet(models.Model):
|
||||
return res
|
||||
|
||||
@api.one
|
||||
@api.constrains('expense_line_ids')
|
||||
@api.constrains('expense_line_ids', 'employee_id')
|
||||
def _check_employee(self):
|
||||
employee_ids = self.expense_line_ids.mapped('employee_id')
|
||||
if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != self.employee_id):
|
||||
|
||||
@@ -311,7 +311,10 @@ class HrPayslip(models.Model):
|
||||
baselocaldict = {'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, 'inputs': inputs}
|
||||
#get the ids of the structures on the contracts and their parent id as well
|
||||
contracts = self.env['hr.contract'].browse(contract_ids)
|
||||
structure_ids = contracts.get_all_structures()
|
||||
if len(contracts) == 1 and payslip.struct_id:
|
||||
structure_ids = list(set(payslip.struct_id._get_parent_structure().ids))
|
||||
else:
|
||||
structure_ids = contracts.get_all_structures()
|
||||
#get the rules of the structure and thier children
|
||||
rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules()
|
||||
#run the rules by sequence
|
||||
|
||||
@@ -26,14 +26,18 @@ CrashManager.include({
|
||||
credit: error_data.credit,
|
||||
}
|
||||
}).then(function (url) {
|
||||
var content = $(QWeb.render('iap.redirect_to_odoo_credit', {
|
||||
data: error_data,
|
||||
}))
|
||||
if (error_data.body) {
|
||||
content.css('padding', 0);
|
||||
}
|
||||
new Dialog(this, {
|
||||
size: 'large',
|
||||
title: error_data.title || _t("Insufficient Balance"),
|
||||
$content: $(QWeb.render('iap.redirect_to_odoo_credit', {
|
||||
data: error_data,
|
||||
})).css('padding', 0),
|
||||
$content: content,
|
||||
buttons: [
|
||||
{text: 'Buy credits at Odoo', classes : "btn-primary", click: function() {
|
||||
{text: _t('Buy credits at Odoo'), classes : "btn-primary", click: function() {
|
||||
window.open(url, '_blank');
|
||||
}, close:true},
|
||||
{text: _t("Cancel"), close: true}
|
||||
|
||||
@@ -373,7 +373,7 @@ elif salary <= 2461.27:
|
||||
<field name="code">Red.Spouse.Net</field>
|
||||
<field name="sequence">148</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_net_revenue <= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue <= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = min(110.50, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
|
||||
</record>
|
||||
@@ -384,7 +384,7 @@ elif salary <= 2461.27:
|
||||
<field name="code">Red.Spouse.Oth.Net</field>
|
||||
<field name="sequence">149</field>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = (employee.marital == 'married' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue <= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue <= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = min(220.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
|
||||
</record>
|
||||
|
||||
@@ -215,23 +215,23 @@ class HrEmployee(models.Model):
|
||||
spouse_fiscal_status = fields.Selection([
|
||||
('without income', 'Without Income'),
|
||||
('with income', 'With Income')
|
||||
], string='Tax status for spouse')
|
||||
disabled = fields.Boolean(string="Disabled", help="If the employee is declared disabled by law")
|
||||
disabled_spouse_bool = fields.Boolean(string='Disabled Spouse', help='if recipient spouse is declared disabled by law')
|
||||
disabled_children_bool = fields.Boolean(string='Disabled Children', help='if recipient children is/are declared disabled by law')
|
||||
resident_bool = fields.Boolean(string='Nonresident', help='if recipient lives in a foreign country')
|
||||
disabled_children_number = fields.Integer('Number of disabled children')
|
||||
dependent_children = fields.Integer(compute='_compute_dependent_children', string='Considered number of dependent children')
|
||||
other_dependent_people = fields.Boolean(string="Other Dependent People", help="If other people are dependent on the employee")
|
||||
other_senior_dependent = fields.Integer('# seniors (>=65)', help="Number of seniors dependent on the employee, including the disabled ones")
|
||||
other_disabled_senior_dependent = fields.Integer('# disabled seniors (>=65)')
|
||||
other_juniors_dependent = fields.Integer('# people (<65)', help="Number of juniors dependent on the employee, including the disabled ones")
|
||||
other_disabled_juniors_dependent = fields.Integer('# disabled people (<65)')
|
||||
dependent_seniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent seniors")
|
||||
dependent_juniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent juniors")
|
||||
spouse_net_revenue = fields.Float(string="Spouse Net Revenue", help="Own professional income, other than pensions, annuities or similar income")
|
||||
], string='Tax status for spouse', groups="hr.group_hr_user")
|
||||
disabled = fields.Boolean(string="Disabled", help="If the employee is declared disabled by law", groups="hr.group_hr_user")
|
||||
disabled_spouse_bool = fields.Boolean(string='Disabled Spouse', help='if recipient spouse is declared disabled by law', groups="hr.group_hr_user")
|
||||
disabled_children_bool = fields.Boolean(string='Disabled Children', help='if recipient children is/are declared disabled by law', groups="hr.group_hr_user")
|
||||
resident_bool = fields.Boolean(string='Nonresident', help='if recipient lives in a foreign country', groups="hr.group_hr_user")
|
||||
disabled_children_number = fields.Integer('Number of disabled children', groups="hr.group_hr_user")
|
||||
dependent_children = fields.Integer(compute='_compute_dependent_children', string='Considered number of dependent children', groups="hr.group_hr_user")
|
||||
other_dependent_people = fields.Boolean(string="Other Dependent People", help="If other people are dependent on the employee", groups="hr.group_hr_user")
|
||||
other_senior_dependent = fields.Integer('# seniors (>=65)', help="Number of seniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user")
|
||||
other_disabled_senior_dependent = fields.Integer('# disabled seniors (>=65)', groups="hr.group_hr_user")
|
||||
other_juniors_dependent = fields.Integer('# people (<65)', help="Number of juniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user")
|
||||
other_disabled_juniors_dependent = fields.Integer('# disabled people (<65)', groups="hr.group_hr_user")
|
||||
dependent_seniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent seniors", groups="hr.group_hr_user")
|
||||
dependent_juniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent juniors", groups="hr.group_hr_user")
|
||||
spouse_net_revenue = fields.Float(string="Spouse Net Revenue", help="Own professional income, other than pensions, annuities or similar income", groups="hr.group_hr_user")
|
||||
spouse_other_net_revenue = fields.Float(string="Spouse Other Net Revenue",
|
||||
help='Own professional income which is exclusively composed of pensions, annuities or similar income')
|
||||
help='Own professional income which is exclusively composed of pensions, annuities or similar income', groups="hr.group_hr_user")
|
||||
|
||||
@api.constrains('spouse_fiscal_status', 'spouse_net_revenue', 'spouse_other_net_revenue')
|
||||
def _check_spouse_revenue(self):
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<field name="disabled_children_bool"/>
|
||||
<field name="disabled_children_number" attrs="{'invisible':[('disabled_children_bool','!=',True)]}" colspan="1" help="number of dependent children declared as disabled"/>
|
||||
<field name="dependent_children" invisible="1"/>
|
||||
<field name="other_dependent_people"/>
|
||||
<field name="other_dependent_people" groups="hr.group_hr_user"/>
|
||||
<field name="other_senior_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
<field name="other_disabled_senior_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
<field name="other_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
|
||||
|
||||
@@ -57,11 +57,12 @@ class FleetVehicle(models.Model):
|
||||
for car in self:
|
||||
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
|
||||
|
||||
@api.depends('model_id', 'license_plate', 'log_contracts', 'total_depreciated_cost', 'acquisition_date')
|
||||
@api.depends('model_id', 'license_plate', 'log_contracts', 'acquisition_date',
|
||||
'co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated')
|
||||
def _compute_vehicle_name(self):
|
||||
super(FleetVehicle, self)._compute_vehicle_name()
|
||||
for vehicle in self:
|
||||
acquisition_date = self._get_acquisition_date()
|
||||
acquisition_date = vehicle._get_acquisition_date()
|
||||
vehicle.name += u" \u2022 " + str(round(vehicle.total_depreciated_cost, 2)) + u" \u2022 " + acquisition_date
|
||||
|
||||
def _get_acquisition_date(self):
|
||||
@@ -138,14 +139,14 @@ class FleetVehicleModel(models.Model):
|
||||
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated
|
||||
|
||||
@api.multi
|
||||
@api.depends('name', 'brand_id')
|
||||
@api.depends('name', 'brand_id', 'default_total_depreciated_cost')
|
||||
def name_get(self):
|
||||
res = super(FleetVehicleModel, self).name_get()
|
||||
new_res = []
|
||||
for res_item in res:
|
||||
model = self.browse(res_item[0])
|
||||
if model.default_total_depreciated_cost != 0.0:
|
||||
new_res.append((res_item[0], res_item[1] + u" \u2022 " + str(model.default_total_depreciated_cost)))
|
||||
new_res.append((res_item[0], res_item[1] + u" \u2022 " + str(round(model.default_total_depreciated_cost, 2))))
|
||||
else:
|
||||
new_res.append(res_item)
|
||||
return new_res
|
||||
|
||||
@@ -11,8 +11,8 @@ class HrContract(models.Model):
|
||||
domain=lambda self: self._get_available_cars_domain(),
|
||||
default=lambda self: self.env['fleet.vehicle'].search([('driver_id', '=', self.employee_id.address_home_id.id)], limit=1),
|
||||
help="Employee's company car.")
|
||||
car_atn = fields.Float(compute='_compute_car_atn_and_costs', string='ATN Company Car')
|
||||
company_car_total_depreciated_cost = fields.Float(compute='_compute_car_atn_and_costs')
|
||||
car_atn = fields.Float(compute='_compute_car_atn_and_costs', string='ATN Company Car', store=True)
|
||||
company_car_total_depreciated_cost = fields.Float(compute='_compute_car_atn_and_costs', store=True)
|
||||
available_cars_amount = fields.Integer(compute='_compute_available_cars_amount', string='Number of available cars')
|
||||
new_car = fields.Boolean('Request a new car')
|
||||
new_car_model_id = fields.Many2one('fleet.vehicle.model', string="Model", domain=lambda self: self._get_possible_model_domain())
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<field name="car_id" nolabel="1" attrs="{'invisible': [('new_car', '=', True)]}"/>
|
||||
<field name="available_cars_amount" invisible="1"/>
|
||||
</div>
|
||||
<field name="new_car_model_id" attrs="{'invisible': [('new_car', '=', False)]}"/>
|
||||
<field name="new_car_model_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', False)]}"/>
|
||||
<field name="max_unused_cars" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='car_atn']" position="after">
|
||||
|
||||
@@ -15,7 +15,7 @@ This module defines a chart of account for Switzerland (Swiss PME/KMU 2015), tax
|
||||
An ISR will be generated if you specify the information it needs :
|
||||
- The bank account you expect to be paid on must be set, and have a valid postal reference.
|
||||
- Your invoice must have been set assigned a bank account to receive its payment
|
||||
(this can be done manually, but a default value is automatically set if you have defined a bank account).
|
||||
(this can be done manually, but a default value is automatically set if you have defined a bank account).
|
||||
- You must have set the postal references of your bank.
|
||||
- Your invoice must be in EUR or CHF (as ISRs do not accept other currencies)
|
||||
|
||||
|
||||
@@ -9,9 +9,15 @@
|
||||
<record id="tax_group_tva_25" model="account.tax.group">
|
||||
<field name="name">TVA 2.5%</field>
|
||||
</record>
|
||||
<record id="tax_group_tva_37" model="account.tax.group">
|
||||
<field name="name">TVA 3.7%</field>
|
||||
</record>
|
||||
<record id="tax_group_tva_38" model="account.tax.group">
|
||||
<field name="name">TVA 3.8%</field>
|
||||
</record>
|
||||
<record id="tax_group_tva_77" model="account.tax.group">
|
||||
<field name="name">TVA 7.7%</field>
|
||||
</record>
|
||||
<record id="tax_group_tva_8" model="account.tax.group">
|
||||
<field name="name">TVA 8%</field>
|
||||
</record>
|
||||
|
||||
@@ -19,6 +19,17 @@
|
||||
<field name="tax_dest_id" ref="vat_O_import" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_5" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_37_purchase" />
|
||||
<field name="tax_dest_id" ref="vat_O_import" />
|
||||
</record>
|
||||
<record id="fiscal_position_tax_template_6" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_37_invest" />
|
||||
<field name="tax_dest_id" ref="vat_O_import" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_7" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_38_purchase" />
|
||||
@@ -30,6 +41,17 @@
|
||||
<field name="tax_dest_id" ref="vat_O_import" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_9" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_77_purchase" />
|
||||
<field name="tax_dest_id" ref="vat_O_import" />
|
||||
</record>
|
||||
<record id="fiscal_position_tax_template_10" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_77_invest" />
|
||||
<field name="tax_dest_id" ref="vat_O_import" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_11" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_80_purchase" />
|
||||
@@ -47,12 +69,24 @@
|
||||
<field name="tax_dest_id" ref="vat_XO" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_15" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_37" />
|
||||
<field name="tax_dest_id" ref="vat_XO" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_16" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_38" />
|
||||
<field name="tax_dest_id" ref="vat_XO" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_17" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_77" />
|
||||
<field name="tax_dest_id" ref="vat_XO" />
|
||||
</record>
|
||||
|
||||
<record id="fiscal_position_tax_template_18" model="account.fiscal.position.tax.template">
|
||||
<field name="position_id" ref="fiscal_position_template_import" />
|
||||
<field name="tax_src_id" ref="vat_80" />
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
<field name="name">Switzerland VAT Form: grid 301 base</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_302_a" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 302 base</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_311_a" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 311 base</field>
|
||||
<field name="applicability">taxes</field>
|
||||
@@ -40,6 +44,10 @@
|
||||
<field name="name">Switzerland VAT Form: grid 341 base</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_342_a" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 342 base</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_381_a" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 381 base</field>
|
||||
<field name="applicability">taxes</field>
|
||||
@@ -48,6 +56,10 @@
|
||||
<field name="name">Switzerland VAT Form: grid 301 tax</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_302_b" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 302 tax</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_311_b" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 311 tax</field>
|
||||
<field name="applicability">taxes</field>
|
||||
@@ -56,6 +68,10 @@
|
||||
<field name="name">Switzerland VAT Form: grid 341 tax</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_342_b" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 342 tax</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="vat_tag_381_b" model="account.account.tag">
|
||||
<field name="name">Switzerland VAT Form: grid 381 tax</field>
|
||||
<field name="applicability">taxes</field>
|
||||
@@ -166,6 +182,81 @@
|
||||
<field name="tax_group_id" ref="tax_group_tva_25"/>
|
||||
</record>
|
||||
|
||||
<record model="account.tax.template" id="vat_37">
|
||||
<field name="name">TVA due a 3.7% (TS)</field>
|
||||
<field name="description">3.7%</field>
|
||||
<field name="amount" eval="3.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="refund_account_id" ref="ch_coa_2200"/>
|
||||
<field name="account_id" ref="ch_coa_2200"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_342_a'), ref('vat_tag_342_b')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_37"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_37_incl">
|
||||
<field name="name">TVA due à 3.7% (Incl. TS)</field>
|
||||
<field name="description">3.7% Incl.</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="3.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="refund_account_id" ref="ch_coa_2200"/>
|
||||
<field name="account_id" ref="ch_coa_2200"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_342_a'), ref('vat_tag_342_b')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_37"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_37_purchase">
|
||||
<field name="name">TVA 3.7% sur achat B&S (TS)</field>
|
||||
<field name="description">3.7% achat</field>
|
||||
<field name="amount" eval="3.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1170"/>
|
||||
<field name="account_id" ref="ch_coa_1170"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_400')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_37"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_37_purchase_incl">
|
||||
<field name="name">TVA 3.7% sur achat B&S (Incl. TS)</field>
|
||||
<field name="description">3.7% achat Incl.</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="3.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1170"/>
|
||||
<field name="account_id" ref="ch_coa_1170"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_400')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_37"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_37_invest">
|
||||
<field name="name">TVA 3.7% sur invest. et autres ch. (TS)</field>
|
||||
<field name="description">3.7% invest</field>
|
||||
<field name="amount" eval="3.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1171"/>
|
||||
<field name="account_id" ref="ch_coa_1171"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_405')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_37"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_37_invest_incl">
|
||||
<field name="name">TVA 3.7% sur invest. et autres ch. (Incl. TS)</field>
|
||||
<field name="description">3.7% invest Incl.</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="3.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1171"/>
|
||||
<field name="account_id" ref="ch_coa_1171"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_405')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_37"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_38">
|
||||
<field name="name">TVA due a 3.8% (TS)</field>
|
||||
<field name="description">3.8%</field>
|
||||
@@ -241,6 +332,85 @@
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_405')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_38"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_77">
|
||||
<field name="name">TVA due a 7.7% (TN)</field>
|
||||
<field name="description">7.7%</field>
|
||||
<field name="amount" eval="7.7"/>
|
||||
<field name="sequence" eval="0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="refund_account_id" ref="ch_coa_2200"/>
|
||||
<field name="account_id" ref="ch_coa_2200"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_302_a'), ref('vat_tag_302_b')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_77"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_77_incl">
|
||||
<field name="name">TVA due à 7.7% (Incl. TN)</field>
|
||||
<field name="description">7.7% Incl.</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="7.7"/>
|
||||
<field name="sequence" eval="0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="refund_account_id" ref="ch_coa_2200"/>
|
||||
<field name="account_id" ref="ch_coa_2200"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_302_a'), ref('vat_tag_302_b')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_77"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_77_purchase">
|
||||
<field name="name">TVA 7.7% sur achat B&S (TN)</field>
|
||||
<field name="description">7.7% achat</field>
|
||||
<field name="amount" eval="7.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="sequence" eval="0"/>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1170"/>
|
||||
<field name="account_id" ref="ch_coa_1170"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_400')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_77"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_77_purchase_incl">
|
||||
<field name="name">TVA 7.7% sur achat B&S (Incl. TN)</field>
|
||||
<field name="description">7.7% achat Incl.</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="7.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="sequence" eval="0"/>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1170"/>
|
||||
<field name="account_id" ref="ch_coa_1170"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_400')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_77"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_77_invest">
|
||||
<field name="name">TVA 7.7% sur invest. et autres ch. (TN)</field>
|
||||
<field name="description">7.7% invest.</field>
|
||||
<field name="amount" eval="7.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1171"/>
|
||||
<field name="account_id" ref="ch_coa_1171"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_405')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_77"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_77_invest_incl">
|
||||
<field name="name">TVA 7.7% sur invest. et autres ch. (Incl. TN)</field>
|
||||
<field name="description">7.7% invest. Incl.</field>
|
||||
<field name="price_include" eval="1"/>
|
||||
<field name="amount" eval="7.7"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="chart_template_id" ref="l10nch_chart_template"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="refund_account_id" ref="ch_coa_1171"/>
|
||||
<field name="account_id" ref="ch_coa_1171"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('vat_tag_405')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_tva_77"/>
|
||||
</record>
|
||||
<record model="account.tax.template" id="vat_80">
|
||||
<field name="name">TVA due a 8.0% (TN)</field>
|
||||
<field name="description">8.0%</field>
|
||||
|
||||
@@ -59,10 +59,11 @@ class AccountInvoice(models.Model):
|
||||
""" The ISR reference number is 27 characters long. The first 12 of them
|
||||
contain the postal account number of this ISR's issuer, removing the zeros
|
||||
at the beginning and filling the empty places with zeros on the right if it is
|
||||
too short. The 15 other characters contain an internal reference identifying
|
||||
too short. The next 14 characters contain an internal reference identifying
|
||||
the invoice. For this, we use the invoice sequence number, removing each
|
||||
of its non-digit characters, and pad the unused spaces on the left of
|
||||
this number with zeros.
|
||||
this number with zeros. The last character of the ISR number is the result
|
||||
of a recursive modulo 10 on its first 26 characters.
|
||||
"""
|
||||
def _space_isr_number(isr_number):
|
||||
to_treat = isr_number
|
||||
@@ -81,9 +82,9 @@ class AccountInvoice(models.Model):
|
||||
invoice_ref = re.sub('[^\d]', '', record.number)
|
||||
#We only keep the last digits of the sequence number if it is too long
|
||||
invoice_ref = invoice_ref[-l10n_ch_ISR_NUMBER_ISSUER_LENGTH:]
|
||||
internal_ref = invoice_ref.zfill(l10n_ch_ISR_NUMBER_LENGTH - l10n_ch_ISR_NUMBER_ISSUER_LENGTH)
|
||||
internal_ref = invoice_ref.zfill(l10n_ch_ISR_NUMBER_LENGTH - l10n_ch_ISR_NUMBER_ISSUER_LENGTH - 1) # -1 for mod10r check character
|
||||
|
||||
record.l10n_ch_isr_number = invoice_issuer_ref + internal_ref
|
||||
record.l10n_ch_isr_number = mod10r(invoice_issuer_ref + internal_ref)
|
||||
record.l10n_ch_isr_number_spaced = _space_isr_number(record.l10n_ch_isr_number)
|
||||
|
||||
@api.depends('currency_id.name', 'amount_total', 'partner_bank_id.bank_id', 'number', 'partner_bank_id.l10n_ch_postal', 'partner_bank_id.bank_id.l10n_ch_postal_eur', 'partner_bank_id.bank_id.l10n_ch_postal_chf')
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from .models.res_company import UNALTERABLE_COUNTRIES
|
||||
|
||||
|
||||
def _setup_inalterability(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
# enable ping for this module
|
||||
env['publisher_warranty.contract'].update_notification(cron_mode=True)
|
||||
|
||||
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)])
|
||||
if fr_companies:
|
||||
# create the securisation sequence per company
|
||||
fr_companies._create_secure_sequence(['l10n_fr_secure_sequence_id'])
|
||||
|
||||
#reset the update_posted field on journals
|
||||
journals = env['account.journal'].search([('company_id', 'in', fr_companies.ids)])
|
||||
journals.write({'update_posted': False})
|
||||
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'France - VAT Anti-Fraud Certification (CGI 286 I-3 bis)',
|
||||
'version': '1.0',
|
||||
'category': 'Localization',
|
||||
'description': """
|
||||
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
The module adds following features:
|
||||
|
||||
Inalterability: deactivation of all the ways to cancel or modify key data, invoices and journal entries
|
||||
|
||||
Security: chaining algorithm to verify the inalterability
|
||||
|
||||
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
|
||||
|
||||
Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users)
|
||||
""",
|
||||
'depends': ['l10n_fr'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'data': [
|
||||
'data/account_move.xml',
|
||||
'views/res_config.xml',
|
||||
],
|
||||
'post_init_hook': '_setup_inalterability',
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<odoo>
|
||||
<record model="ir.actions.server" id="action_check_hash_integrity">
|
||||
<field name="name">Data Inalterability Check</field>
|
||||
<field name="model_id" ref="account.model_account_move"/>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="code">
|
||||
action = env['account.move']._check_hash_integrity(env.user.company_id.id)
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="odoo_french_certification">
|
||||
<field name="name">Get the certification now!</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="account.model_res_company"/>
|
||||
<field name="code">
|
||||
db_uuid = env['ir.config_parameter'].sudo().get_param('database.uuid')
|
||||
action = {"type": "ir.actions.act_url",
|
||||
"url": "https://www.odoo.com/my/contract/french-certification/db/" + db_uuid,
|
||||
"target": "new",}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_check_hash_integrity" id="menu_check_move_integrity_reporting" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import res_company
|
||||
from . import account
|
||||
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from hashlib import sha256
|
||||
from json import dumps
|
||||
|
||||
from odoo import models, api, fields
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
#forbidden fields
|
||||
MOVE_FIELDS = ['date', 'journal_id', 'company_id']
|
||||
LINE_FIELDS = ['debit', 'credit', 'account_id', 'partner_id']
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
# TO DO in master : refactor hashing algo to go into a mixin
|
||||
|
||||
l10n_fr_secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False)
|
||||
l10n_fr_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False)
|
||||
l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False)
|
||||
|
||||
def _get_new_hash(self, secure_seq_number):
|
||||
""" Returns the hash to write on journal entries when they get posted"""
|
||||
self.ensure_one()
|
||||
#get the only one exact previous move in the securisation sequence
|
||||
prev_move = self.search([('state', '=', 'posted'),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('l10n_fr_secure_sequence_number', '!=', 0),
|
||||
('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)])
|
||||
if prev_move and len(prev_move) != 1:
|
||||
raise UserError(
|
||||
_('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.'))
|
||||
|
||||
#build and return the hash
|
||||
return self._compute_hash(prev_move.l10n_fr_hash if prev_move else '')
|
||||
|
||||
def _compute_hash(self, previous_hash):
|
||||
""" Computes the hash of the browse_record given as self, based on the hash
|
||||
of the previous record in the company's securisation sequence given as parameter"""
|
||||
self.ensure_one()
|
||||
hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
|
||||
return hash_string.hexdigest()
|
||||
|
||||
def _compute_string_to_hash(self):
|
||||
def _getattrstring(obj, field_str):
|
||||
field_value = obj[field_str]
|
||||
if obj._fields[field_str].type == 'many2one':
|
||||
field_value = field_value.id
|
||||
return str(field_value)
|
||||
|
||||
for move in self:
|
||||
values = {}
|
||||
for field in MOVE_FIELDS:
|
||||
values[field] = _getattrstring(move, field)
|
||||
|
||||
for line in move.line_ids:
|
||||
for field in LINE_FIELDS:
|
||||
k = 'line_%d_%s' % (line.id, field)
|
||||
values[k] = _getattrstring(line, field)
|
||||
#make the json serialization canonical
|
||||
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
|
||||
move.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
|
||||
ensure_ascii=True, indent=None,
|
||||
separators=(',',':'))
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
has_been_posted = False
|
||||
for move in self:
|
||||
if move.company_id._is_accounting_unalterable():
|
||||
# write the hash and the secure_sequence_number when posting an account.move
|
||||
if vals.get('state') == 'posted':
|
||||
has_been_posted = True
|
||||
|
||||
# restrict the operation in case we are trying to write a forbidden field
|
||||
if (move.state == "posted" and set(vals).intersection(MOVE_FIELDS)):
|
||||
raise UserError(_("According to the French law, you cannot modify a journal entry in order for its posted data to be updated or deleted. Unauthorized field: %s.") % ', '.join(MOVE_FIELDS))
|
||||
# restrict the operation in case we are trying to overwrite existing hash
|
||||
if (move.l10n_fr_hash and 'l10n_fr_hash' in vals) or (move.l10n_fr_secure_sequence_number and 'l10n_fr_secure_sequence_number' in vals):
|
||||
raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.'))
|
||||
res = super(AccountMove, self).write(vals)
|
||||
# write the hash and the secure_sequence_number when posting an account.move
|
||||
if has_been_posted:
|
||||
for move in self.filtered(lambda m: m.company_id._is_accounting_unalterable() and
|
||||
not (m.l10n_fr_secure_sequence_number or m.l10n_fr_hash)):
|
||||
new_number = move.company_id.l10n_fr_secure_sequence_id.next_by_id()
|
||||
vals_hashing = {'l10n_fr_secure_sequence_number': new_number,
|
||||
'l10n_fr_hash': move._get_new_hash(new_number)}
|
||||
res |= super(AccountMove, move).write(vals_hashing)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def button_cancel(self):
|
||||
#by-pass the normal behavior/message that tells people can cancel a posted journal entry
|
||||
#if the journal allows it.
|
||||
if self.company_id._is_accounting_unalterable():
|
||||
raise UserError(_('You cannot modify a posted journal entry. This ensures its inalterability.'))
|
||||
super(AccountMove, self).button_cancel()
|
||||
|
||||
@api.model
|
||||
def _check_hash_integrity(self, company_id):
|
||||
"""Checks that all posted moves have still the same data as when they were posted
|
||||
and raises an error with the result.
|
||||
"""
|
||||
def build_move_info(move):
|
||||
entry_reference = _('(ref.: %s)')
|
||||
move_reference_string = move.ref and entry_reference % move.ref or ''
|
||||
return [move.name, move_reference_string]
|
||||
|
||||
moves = self.search([('state', '=', 'posted'),
|
||||
('company_id', '=', company_id),
|
||||
('l10n_fr_secure_sequence_number', '!=', 0)],
|
||||
order="l10n_fr_secure_sequence_number ASC")
|
||||
|
||||
if not moves:
|
||||
raise UserError(_('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.') % self.env.user.company_id.name)
|
||||
previous_hash = ''
|
||||
start_move_info = []
|
||||
for move in moves:
|
||||
if move.l10n_fr_hash != move._compute_hash(previous_hash=previous_hash):
|
||||
raise UserError(_('Corrupted data on journal entry with id %s.') % move.id)
|
||||
if not previous_hash:
|
||||
#save the date and sequence number of the first move hashed
|
||||
start_move_info = build_move_info(move)
|
||||
previous_hash = move.l10n_fr_hash
|
||||
end_move_info = build_move_info(move)
|
||||
|
||||
report_dict = {'start_move_name': start_move_info[0],
|
||||
'start_move_ref': start_move_info[1],
|
||||
'end_move_name': end_move_info[0],
|
||||
'end_move_ref': end_move_info[1]}
|
||||
|
||||
# Raise on success
|
||||
raise UserError(_('''Successful test !
|
||||
|
||||
The journal entries are guaranteed to be in their original and inalterable state
|
||||
From: %(start_move_name)s %(start_move_ref)s
|
||||
To: %(end_move_name)s %(end_move_ref)s
|
||||
|
||||
For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users).'''
|
||||
) % report_dict)
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = "account.move.line"
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# restrict the operation in case we are trying to write a forbidden field
|
||||
if set(vals).intersection(LINE_FIELDS):
|
||||
if any(l.company_id._is_accounting_unalterable() and l.move_id.state == 'posted' for l in self):
|
||||
raise UserError(_("According to the French law, you cannot modify a journal item in order for its posted data to be updated or deleted. Unauthorized field: %s.") % ', '.join(LINE_FIELDS))
|
||||
return super(AccountMoveLine, self).write(vals)
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = "account.journal"
|
||||
|
||||
@api.onchange('update_posted')
|
||||
def _onchange_update_posted(self):
|
||||
if self.update_posted and self.company_id._is_accounting_unalterable():
|
||||
field_string = self._fields['update_posted'].get_description(self.env)['string']
|
||||
raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string)
|
||||
|
||||
@api.multi
|
||||
def _is_journal_alterable(self):
|
||||
self.ensure_one()
|
||||
critical_domain = [('journal_id', '=', self.id),
|
||||
'|', ('l10n_fr_hash', '!=', False),
|
||||
'&', ('l10n_fr_secure_sequence_number', '!=', False),
|
||||
('l10n_fr_secure_sequence_number', '!=', 0)]
|
||||
if self.env['account.move'].search(critical_domain):
|
||||
raise UserError(_('It is not permitted to disable the data inalterability in this journal (%s) since journal entries have already been protected.') % (self.name, ))
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# restrict the operation in case we are trying to write a forbidden field
|
||||
for journal in self:
|
||||
if journal.company_id._is_accounting_unalterable():
|
||||
if vals.get('update_posted'):
|
||||
field_string = journal._fields['update_posted'].get_description(self.env)['string']
|
||||
raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string)
|
||||
return super(AccountJournal, self).write(vals)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# restrict the operation in case we are trying to set a forbidden field
|
||||
if self.company_id._is_accounting_unalterable():
|
||||
if vals.get('update_posted'):
|
||||
field_string = self._fields['update_posted'].get_description(self.env)['string']
|
||||
raise UserError(_("According to the French law, you cannot modify a journal in order for its posted data to be updated or deleted. Unauthorized field: %s.") % field_string)
|
||||
return super(AccountJournal, self).create(vals)
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
UNALTERABLE_COUNTRIES = ['FR', 'MF', 'MQ', 'NC', 'PF', 'RE', 'GF', 'GP', 'TF']
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
# To do in master : refactor to set sequences more generic
|
||||
|
||||
l10n_fr_secure_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to ensure the securisation of data', readonly=True)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = super(ResCompany, self).create(vals)
|
||||
#when creating a new french company, create the securisation sequence as well
|
||||
if company._is_accounting_unalterable():
|
||||
sequence_fields = ['l10n_fr_secure_sequence_id']
|
||||
company._create_secure_sequence(sequence_fields)
|
||||
return company
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(ResCompany, self).write(vals)
|
||||
#if country changed to fr, create the securisation sequence
|
||||
for company in self:
|
||||
if company._is_accounting_unalterable():
|
||||
sequence_fields = ['l10n_fr_secure_sequence_id']
|
||||
company._create_secure_sequence(sequence_fields)
|
||||
return res
|
||||
|
||||
def _create_secure_sequence(self, sequence_fields):
|
||||
"""This function creates a no_gap sequence on each companies in self that will ensure
|
||||
a unique number is given to all posted account.move in such a way that we can always
|
||||
find the previous move of a journal entry.
|
||||
"""
|
||||
for company in self:
|
||||
vals_write = {}
|
||||
for seq_field in sequence_fields:
|
||||
if not company[seq_field]:
|
||||
vals = {
|
||||
'name': 'French Securisation of ' + seq_field + ' - ' + company.name,
|
||||
'code': 'FRSECUR',
|
||||
'implementation': 'no_gap',
|
||||
'prefix': '',
|
||||
'suffix': '',
|
||||
'padding': 0,
|
||||
'company_id': company.id}
|
||||
seq = self.env['ir.sequence'].create(vals)
|
||||
vals_write[seq_field] = seq.id
|
||||
if vals_write:
|
||||
company.write(vals_write)
|
||||
|
||||
def _is_vat_french(self):
|
||||
return self.vat and self.vat.startswith('FR') and len(self.vat) == 13
|
||||
|
||||
def _is_accounting_unalterable(self):
|
||||
if not self.vat and not self.country_id:
|
||||
return False
|
||||
return self.country_id and self.country_id.code in UNALTERABLE_COUNTRIES or self._is_vat_french()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 562 B |
@@ -0,0 +1,18 @@
|
||||
<odoo>
|
||||
<record id="view_general_fr_inalterability_inherit_config" model="ir.ui.view">
|
||||
<field name="name">Inalterability settings</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='company_id']" position="before">
|
||||
<h2>Data Inalterability</h2>
|
||||
<div>
|
||||
<div>
|
||||
<button name="%(l10n_fr_certification.action_check_hash_integrity)d" type="action" string="Data Inalterability Check" class="oe_link"/>
|
||||
<button name="%(l10n_fr_certification.odoo_french_certification)d" type="action" string="Get the certification now!" class="oe_link"/>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from ..l10n_fr_certification.models.res_company import UNALTERABLE_COUNTRIES
|
||||
|
||||
|
||||
def _setup_inalterability(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
# enable ping for this module
|
||||
env['publisher_warranty.contract'].update_notification(cron_mode=True)
|
||||
|
||||
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)])
|
||||
if fr_companies:
|
||||
# create the securisation sequence per company
|
||||
fr_companies._create_secure_sequence(['l10n_fr_pos_cert_sequence_id'])
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'France - VAT Anti-Fraud Certification for Point of Sale (CGI 286 I-3 bis)',
|
||||
'version': '1.0',
|
||||
'category': 'Localization',
|
||||
'description': """
|
||||
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
Install it if you use the Point of Sale app to sell to individuals.
|
||||
|
||||
The module adds following features:
|
||||
|
||||
Inalterability: deactivation of all the ways to cancel or modify key data of POS orders, invoices and journal entries
|
||||
|
||||
Security: chaining algorithm to verify the inalterability
|
||||
|
||||
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
|
||||
|
||||
Access to download the mandatory Certificate of Conformity delivered by Odoo SA (only for Odoo Enterprise users)
|
||||
""",
|
||||
'depends': ['l10n_fr_sale_closing', 'point_of_sale'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
'data': [
|
||||
'data/pos_inalterability.xml',
|
||||
'views/account_views.xml',
|
||||
],
|
||||
'post_init_hook': '_setup_inalterability',
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<odoo>
|
||||
|
||||
<menuitem id="pos_fr_statements_menu" name="French Statements" parent="point_of_sale.menu_point_rep" sequence="9" />
|
||||
|
||||
<menuitem action="l10n_fr_sale_closing.action_list_view_account_sale_closing" id="menu_account_closing" parent="pos_fr_statements_menu" sequence="90"/>
|
||||
|
||||
<record model="ir.actions.server" id="action_check_hash_integrity_pos_orders">
|
||||
<field name="name">Pos Orders Inalterability Check</field>
|
||||
<field name="model_id" ref="point_of_sale.model_pos_order"/>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="code">
|
||||
action = env['pos.order']._check_hash_integrity(env.user.company_id.id)
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_check_hash_integrity_pos_orders" id="menu_check_pos_order_integrity" parent="pos_fr_statements_menu" sequence="90"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import account_bank_statement
|
||||
from . import res_company
|
||||
from . import pos
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountBankStatement(models.Model):
|
||||
_inherit = 'account.bank.statement'
|
||||
|
||||
def unlink(self):
|
||||
for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
|
||||
raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name,))
|
||||
return super(AccountBankStatement, self).unlink()
|
||||
|
||||
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = 'account.bank.statement.line'
|
||||
|
||||
def unlink(self):
|
||||
for line in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
|
||||
raise UserError(_('You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations.') % (line.name,))
|
||||
return super(AccountBankStatementLine, self).unlink()
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import sha256
|
||||
from json import dumps
|
||||
import pytz
|
||||
|
||||
from odoo import models, api, fields
|
||||
from odoo.fields import Datetime
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
def ctx_tz(record, field):
|
||||
res_lang = None
|
||||
ctx = record._context
|
||||
tz_name = pytz.timezone(ctx.get('tz') or record.env.user.tz)
|
||||
timestamp = Datetime.from_string(record[field])
|
||||
if ctx.get('lang'):
|
||||
res_lang = record.env['res.lang'].search([('code', '=', ctx['lang'])], limit=1)
|
||||
if res_lang:
|
||||
timestamp = pytz.utc.localize(timestamp, is_dst=False)
|
||||
return datetime.strftime(timestamp.astimezone(tz_name), res_lang.date_format + ' ' + res_lang.time_format)
|
||||
return Datetime.context_timestamp(record, timestamp)
|
||||
|
||||
|
||||
class pos_config(models.Model):
|
||||
_inherit = 'pos.config'
|
||||
|
||||
@api.multi
|
||||
def open_ui(self):
|
||||
for config in self.filtered(lambda c: c.company_id._is_accounting_unalterable()):
|
||||
if config.current_session_id:
|
||||
config.current_session_id._check_session_timing()
|
||||
return super(pos_config, self).open_ui()
|
||||
|
||||
|
||||
class pos_session(models.Model):
|
||||
_inherit = 'pos.session'
|
||||
|
||||
@api.multi
|
||||
def _check_session_timing(self):
|
||||
self.ensure_one()
|
||||
date_today = datetime.utcnow()
|
||||
session_start = Datetime.from_string(self.start_at)
|
||||
if not date_today - timedelta(hours=24) <= session_start:
|
||||
raise UserError(_("This session has been opened another day. To comply with the French law, you should close sessions on a daily basis. Please close session %s and open a new one.") % self.name)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def open_frontend_cb(self):
|
||||
for session in self.filtered(lambda s: s.config_id.company_id._is_accounting_unalterable()):
|
||||
session._check_session_timing()
|
||||
return super(pos_session, self).open_frontend_cb()
|
||||
|
||||
|
||||
ORDER_FIELDS = ['date_order', 'user_id', 'lines', 'statement_ids', 'pricelist_id', 'partner_id', 'session_id', 'pos_reference', 'sale_journal', 'fiscal_position_id']
|
||||
LINE_FIELDS = ['notice', 'product_id', 'qty', 'price_unit', 'discount', 'tax_ids', 'tax_ids_after_fiscal_position']
|
||||
ERR_MSG = _('According to the French law, you cannot modify a %s. Forbidden fields: %s.')
|
||||
|
||||
|
||||
class pos_order(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
l10n_fr_hash = fields.Char(string="Inalteralbility Hash", readonly=True, copy=False)
|
||||
l10n_fr_secure_sequence_number = fields.Integer(string="Inalteralbility No Gap Sequence #", readonly=True, copy=False)
|
||||
l10n_fr_string_to_hash = fields.Char(compute='_compute_string_to_hash', readonly=True, store=False)
|
||||
|
||||
def _get_new_hash(self, secure_seq_number):
|
||||
""" Returns the hash to write on pos orders when they get posted"""
|
||||
self.ensure_one()
|
||||
#get the only one exact previous order in the securisation sequence
|
||||
prev_order = self.search([('state', 'in', ['paid', 'done', 'invoiced']),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('l10n_fr_secure_sequence_number', '!=', 0),
|
||||
('l10n_fr_secure_sequence_number', '=', int(secure_seq_number) - 1)])
|
||||
if prev_order and len(prev_order) != 1:
|
||||
raise UserError(
|
||||
_('An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order.'))
|
||||
|
||||
#build and return the hash
|
||||
return self._compute_hash(prev_order.l10n_fr_hash if prev_order else '')
|
||||
|
||||
def _compute_hash(self, previous_hash):
|
||||
""" Computes the hash of the browse_record given as self, based on the hash
|
||||
of the previous record in the company's securisation sequence given as parameter"""
|
||||
self.ensure_one()
|
||||
hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
|
||||
return hash_string.hexdigest()
|
||||
|
||||
def _compute_string_to_hash(self):
|
||||
def _getattrstring(obj, field_str):
|
||||
field_value = obj[field_str]
|
||||
if obj._fields[field_str].type == 'many2one':
|
||||
field_value = field_value.id
|
||||
if obj._fields[field_str].type in ['many2many', 'one2many']:
|
||||
field_value = field_value.ids
|
||||
return str(field_value)
|
||||
|
||||
for order in self:
|
||||
values = {}
|
||||
for field in ORDER_FIELDS:
|
||||
values[field] = _getattrstring(order, field)
|
||||
|
||||
for line in order.lines:
|
||||
for field in LINE_FIELDS:
|
||||
k = 'line_%d_%s' % (line.id, field)
|
||||
values[k] = _getattrstring(line, field)
|
||||
#make the json serialization canonical
|
||||
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
|
||||
order.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
|
||||
ensure_ascii=True, indent=None,
|
||||
separators=(',',':'))
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
has_been_posted = False
|
||||
for order in self:
|
||||
if order.company_id._is_accounting_unalterable():
|
||||
# write the hash and the secure_sequence_number when posting or invoicing an pos.order
|
||||
if vals.get('state') in ['paid', 'done', 'invoiced']:
|
||||
has_been_posted = True
|
||||
|
||||
# restrict the operation in case we are trying to write a forbidden field
|
||||
if (order.state in ['paid', 'done', 'invoiced'] and set(vals).intersection(ORDER_FIELDS)):
|
||||
raise UserError(_('According to the French law, you cannot modify a point of sale order. Forbidden fields: %s.') % ', '.join(ORDER_FIELDS))
|
||||
# restrict the operation in case we are trying to overwrite existing hash
|
||||
if (order.l10n_fr_hash and 'l10n_fr_hash' in vals) or (order.l10n_fr_secure_sequence_number and 'l10n_fr_secure_sequence_number' in vals):
|
||||
raise UserError(_('You cannot overwrite the values ensuring the inalterability of the point of sale.'))
|
||||
res = super(pos_order, self).write(vals)
|
||||
# write the hash and the secure_sequence_number when posting or invoicing a pos order
|
||||
if has_been_posted:
|
||||
for order in self.filtered(lambda o: o.company_id._is_accounting_unalterable() and
|
||||
not (o.l10n_fr_secure_sequence_number or o.l10n_fr_hash)):
|
||||
new_number = order.company_id.l10n_fr_pos_cert_sequence_id.next_by_id()
|
||||
vals_hashing = {'l10n_fr_secure_sequence_number': new_number,
|
||||
'l10n_fr_hash': order._get_new_hash(new_number)}
|
||||
res |= super(pos_order, order).write(vals_hashing)
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _check_hash_integrity(self, company_id):
|
||||
"""Checks that all posted or invoiced pos orders have still the same data as when they were posted
|
||||
and raises an error with the result.
|
||||
"""
|
||||
def build_order_info(order):
|
||||
entry_reference = _('(Receipt ref.: %s)')
|
||||
order_reference_string = order.pos_reference and entry_reference % order.pos_reference or ''
|
||||
return [ctx_tz(order, 'date_order'), order.l10n_fr_secure_sequence_number, order.name, order_reference_string, ctx_tz(order, 'write_date')]
|
||||
|
||||
orders = self.search([('state', 'in', ['paid', 'done', 'invoiced']),
|
||||
('company_id', '=', company_id),
|
||||
('l10n_fr_secure_sequence_number', '!=', 0)],
|
||||
order="l10n_fr_secure_sequence_number ASC")
|
||||
|
||||
if not orders:
|
||||
raise UserError(_('There isn\'t any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS') % self.env.user.company_id.name)
|
||||
previous_hash = ''
|
||||
start_order_info = []
|
||||
for order in orders:
|
||||
if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
|
||||
raise UserError(_('Corrupted data on point of sale order with id %s.') % order.id)
|
||||
previous_hash = order.l10n_fr_hash
|
||||
|
||||
orders_sorted_date = orders.sorted(lambda o: o.date_order)
|
||||
start_order_info = build_order_info(orders_sorted_date[0])
|
||||
end_order_info = build_order_info(orders_sorted_date[-1])
|
||||
|
||||
report_dict = {'start_order_name': start_order_info[2],
|
||||
'start_order_ref': start_order_info[3],
|
||||
'start_order_date': start_order_info[0],
|
||||
'end_order_name': end_order_info[2],
|
||||
'end_order_ref': end_order_info[3],
|
||||
'end_order_date': end_order_info[0]}
|
||||
|
||||
# Raise on success
|
||||
raise UserError(_('''Successful test !
|
||||
|
||||
The point of sale orders are guaranteed to be in their original and inalterable state
|
||||
From: %(start_order_name)s %(start_order_ref)s recorded on %(start_order_date)s
|
||||
To: %(end_order_name)s %(end_order_ref)s recorded on %(end_order_date)s
|
||||
|
||||
For this report to be legally meaningful, please download your certification from your customer account on Odoo.com (Only for Odoo Enterprise users).'''
|
||||
) % report_dict)
|
||||
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = "pos.order.line"
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
# restrict the operation in case we are trying to write a forbidden field
|
||||
if set(vals).intersection(LINE_FIELDS):
|
||||
if any(l.company_id._is_accounting_unalterable() and l.order_id.state in ['done', 'invoiced'] for l in self):
|
||||
raise UserError(_('According to the French law, you cannot modify a point of sale order line. Forbidden fields: %s.') % ', '.join(LINE_FIELDS))
|
||||
return super(PosOrderLine, self).write(vals)
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models, api, fields
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
l10n_fr_pos_cert_sequence_id = fields.Many2one('ir.sequence')
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = super(ResCompany, self).create(vals)
|
||||
#when creating a new french company, create the securisation sequence as well
|
||||
if company._is_accounting_unalterable():
|
||||
sequence_fields = ['l10n_fr_pos_cert_sequence_id']
|
||||
company._create_secure_sequence(sequence_fields)
|
||||
return company
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(ResCompany, self).write(vals)
|
||||
#if country changed to fr, create the securisation sequence
|
||||
for company in self:
|
||||
if company._is_accounting_unalterable():
|
||||
sequence_fields = ['l10n_fr_pos_cert_sequence_id']
|
||||
company._create_secure_sequence(sequence_fields)
|
||||
return res
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 562 B |
@@ -0,0 +1,26 @@
|
||||
<odoo>
|
||||
<record id="view_bank_statement_form_readonly" model="ir.ui.view">
|
||||
<field name="name">account.bank.statement.form</field>
|
||||
<field name="model">account.bank.statement</field>
|
||||
<field name="inherit_id" ref="account.view_bank_statement_form"/>
|
||||
<field name="mode">extension</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<field name="pos_session_id" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='name']" position="attributes">
|
||||
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='journal_id']" position="attributes">
|
||||
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='date']" position="attributes">
|
||||
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='line_ids']" position="attributes">
|
||||
<attribute name="attrs">{'readonly': [('pos_session_id', '!=', False)]}</attribute>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import models
|
||||
from odoo import api, SUPERUSER_ID
|
||||
from ..l10n_fr_certification.models.res_company import UNALTERABLE_COUNTRIES
|
||||
|
||||
|
||||
def _setup_inalterability(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
# enable ping for this module
|
||||
env['publisher_warranty.contract'].update_notification(cron_mode=True)
|
||||
|
||||
fr_companies = env['res.company'].search([('partner_id.country_id.code', 'in', UNALTERABLE_COUNTRIES)])
|
||||
if fr_companies:
|
||||
# create the securisation sequence per company
|
||||
fr_companies._create_secure_sequence(['l10n_fr_closing_sequence_id'])
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'France - VAT Anti-Fraud Certification (CGI 286 I-3 bis) - Sale Closings',
|
||||
'version': '1.0',
|
||||
'category': 'Localization',
|
||||
'description': """
|
||||
This module adds support for automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
The module adds following features:
|
||||
|
||||
Storage: automatic sales closings with computation of both period and cumulative totals (daily, monthly, annually)
|
||||
|
||||
""",
|
||||
'depends': ['l10n_fr_certification'],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'application': False,
|
||||
'data': [
|
||||
'views/account_sale_closure.xml',
|
||||
'data/account_sale_closure_cron.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/account_closing_intercompany.xml',
|
||||
],
|
||||
'post_init_hook': '_setup_inalterability',
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<odoo>
|
||||
<record model="ir.cron" id="account_sale_closing_daily">
|
||||
<field name="name">Generate Daily Sales Closing</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_account_sale_closing"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._automated_closing('daily')</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="account_sale_closing_monthly">
|
||||
<field name="name">Generate Monthly Sales Closing</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">months</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_account_sale_closing"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._automated_closing('monthly')</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="account_sale_closing_annually">
|
||||
<field name="name">Generate Annual Sales Closing</field>
|
||||
<field name="interval_number">12</field>
|
||||
<field name="interval_type">months</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_account_sale_closing"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._automated_closing('annually')</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import res_company
|
||||
from . import account_closing
|
||||
@@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import models, api, fields
|
||||
from odoo.fields import Datetime as FieldDateTime
|
||||
from odoo.tools.translate import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountClosing(models.Model):
|
||||
"""
|
||||
This object holds an interval total and a grand total of the accounts of type receivable for a company,
|
||||
as well as the last account_move that has been counted in a previous object
|
||||
It takes its earliest brother to infer from when the computation needs to be done
|
||||
in order to compute its own data.
|
||||
"""
|
||||
_name = 'account.sale.closing'
|
||||
_order = 'date_closing_stop desc, sequence_number desc'
|
||||
|
||||
name = fields.Char(help="Frequency and unique sequence number", required=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True)
|
||||
date_closing_stop = fields.Datetime(string="Closing Date", help='Date to which the values are computed', readonly=True, required=True)
|
||||
date_closing_start = fields.Datetime(string="Starting Date", help='Date from which the total interval is computed', readonly=True, required=True)
|
||||
frequency = fields.Selection(string='Closing Type', selection=[('daily', 'Daily'), ('monthly', 'Monthly'), ('annually', 'Annual')], readonly=True, required=True)
|
||||
total_interval = fields.Monetary(string="Period Total", help='Total in receivable accounts during the interval, excluding overlapping periods', readonly=True, required=True)
|
||||
cumulative_total = fields.Monetary(string="Cumulative Grand Total", help='Total in receivable accounts since the beginnig of times', readonly=True, required=True)
|
||||
sequence_number = fields.Integer('Sequence #', readonly=True, required=True)
|
||||
last_move_id = fields.Many2one('account.move', string='Last journal entry', help='Last Journal entry included in the grand total', readonly=True)
|
||||
last_move_hash = fields.Char(string='Last journal entry\'s inalteralbility hash', readonly=True)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency', help="The company's currency", readonly=True, related='company_id.currency_id', store=True)
|
||||
|
||||
def _query_for_aml(self, company, first_move_sequence_number, date_start):
|
||||
params = {'company_id': company.id}
|
||||
query = '''WITH aggregate AS (SELECT m.id AS move_id,
|
||||
aml.balance AS balance,
|
||||
aml.id as line_id
|
||||
FROM account_move_line aml
|
||||
JOIN account_journal j ON aml.journal_id = j.id
|
||||
JOIN account_account acc ON acc.id = aml.account_id
|
||||
JOIN account_account_type t ON (t.id = acc.user_type_id AND t.type = 'receivable')
|
||||
JOIN account_move m ON m.id = aml.move_id
|
||||
WHERE j.type = 'sale'
|
||||
AND aml.company_id = %(company_id)s
|
||||
AND m.state = 'posted' '''
|
||||
|
||||
if first_move_sequence_number is not False and first_move_sequence_number is not None:
|
||||
params['first_move_sequence_number'] = first_move_sequence_number
|
||||
query += '''AND m.l10n_fr_secure_sequence_number > %(first_move_sequence_number)s'''
|
||||
elif date_start:
|
||||
#the first time we compute the closing, we consider only from the installation of the module
|
||||
params['date_start'] = date_start
|
||||
query += '''AND m.date >= %(date_start)s'''
|
||||
|
||||
query += " ORDER BY m.l10n_fr_secure_sequence_number DESC) "
|
||||
query += '''SELECT array_agg(move_id) AS move_ids,
|
||||
array_agg(line_id) AS line_ids,
|
||||
sum(balance) AS balance
|
||||
FROM aggregate'''
|
||||
|
||||
self.env.cr.execute(query, params)
|
||||
return self.env.cr.dictfetchall()[0]
|
||||
|
||||
def _compute_amounts(self, frequency, company):
|
||||
"""
|
||||
Method used to compute all the business data of the new object.
|
||||
It will search for previous closings of the same frequency to infer the move from which
|
||||
account move lines should be fetched.
|
||||
@param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
|
||||
frequencies are literal (daily means 24 hours and so on)
|
||||
@param {recordset} company: the company for which the closing is done
|
||||
@return {dict} containing {field: value} for each business field of the object
|
||||
"""
|
||||
interval_dates = self._interval_dates(frequency, company)
|
||||
previous_closing = self.search([
|
||||
('frequency', '=', frequency),
|
||||
('company_id', '=', company.id)], limit=1, order='sequence_number desc')
|
||||
|
||||
first_move = self.env['account.move']
|
||||
date_start = interval_dates['interval_from']
|
||||
cumulative_total = 0
|
||||
if previous_closing:
|
||||
first_move = previous_closing.last_move_id
|
||||
date_start = previous_closing.create_date
|
||||
cumulative_total += previous_closing.cumulative_total
|
||||
|
||||
aml_aggregate = self._query_for_aml(company, first_move.l10n_fr_secure_sequence_number, date_start)
|
||||
|
||||
total_interval = aml_aggregate['balance'] or 0
|
||||
cumulative_total += total_interval
|
||||
|
||||
# We keep the reference to avoid gaps (like daily object during the weekend)
|
||||
last_move = first_move
|
||||
if aml_aggregate['move_ids']:
|
||||
last_move = last_move.browse(aml_aggregate['move_ids'][0])
|
||||
|
||||
return {'total_interval': total_interval,
|
||||
'cumulative_total': cumulative_total,
|
||||
'last_move_id': last_move.id,
|
||||
'last_move_hash': last_move.l10n_fr_hash,
|
||||
'date_closing_stop': interval_dates['date_stop'],
|
||||
'date_closing_start': date_start,
|
||||
'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10]}
|
||||
|
||||
def _interval_dates(self, frequency, company):
|
||||
"""
|
||||
Method used to compute the theoretical date from which account move lines should be fetched
|
||||
@param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
|
||||
frequencies are literal (daily means 24 hours and so on)
|
||||
@param {recordset} company: the company for which the closing is done
|
||||
@return {dict} the theoretical date from which account move lines are fetched.
|
||||
date_stop date to which the move lines are fetched, always now()
|
||||
the dates are in their Odoo Database string representation
|
||||
"""
|
||||
date_stop = datetime.utcnow()
|
||||
interval_from = None
|
||||
name_interval = ''
|
||||
if frequency == 'daily':
|
||||
interval_from = date_stop - timedelta(days=1)
|
||||
name_interval = _('Daily Closing')
|
||||
elif frequency == 'monthly':
|
||||
month_target = date_stop.month > 1 and date_stop.month - 1 or 12
|
||||
year_target = month_target < 12 and date_stop.year or date_stop.year - 1
|
||||
interval_from = date_stop.replace(year=year_target, month=month_target)
|
||||
name_interval = _('Monthly Closing')
|
||||
elif frequency == 'annually':
|
||||
year_target = date_stop.year - 1
|
||||
interval_from = date_stop.replace(year=year_target)
|
||||
name_interval = _('Annual Closing')
|
||||
|
||||
return {'interval_from': FieldDateTime.to_string(interval_from),
|
||||
'date_stop': FieldDateTime.to_string(date_stop),
|
||||
'name_interval': name_interval}
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
|
||||
|
||||
@api.model
|
||||
def _automated_closing(self, frequency='daily'):
|
||||
"""To be executed by the CRON to create an object of the given frequency for each company that needs it
|
||||
@param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
|
||||
frequencies are literal (daily means 24 hours and so on)
|
||||
@return {recordset} all the objects created for the given frequency
|
||||
"""
|
||||
res_company = self.env['res.company'].search([])
|
||||
account_closings = self.env['account.sale.closing']
|
||||
for company in res_company.filtered(lambda c: c._is_accounting_unalterable()):
|
||||
new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id()
|
||||
values = self._compute_amounts(frequency, company)
|
||||
values['frequency'] = frequency
|
||||
values['company_id'] = company.id
|
||||
values['sequence_number'] = new_sequence_number
|
||||
account_closings |= account_closings.create(values)
|
||||
|
||||
return account_closings
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
l10n_fr_closing_sequence_id = fields.Many2one('ir.sequence', 'Sequence to use to build sale closings', readonly=True)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = super(ResCompany, self).create(vals)
|
||||
#when creating a new french company, create the securisation sequence as well
|
||||
if company._is_accounting_unalterable():
|
||||
sequence_fields = ['l10n_fr_closing_sequence_id']
|
||||
company._create_secure_sequence(sequence_fields)
|
||||
return company
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(ResCompany, self).write(vals)
|
||||
#if country changed to fr, create the securisation sequence
|
||||
for company in self:
|
||||
if company._is_accounting_unalterable():
|
||||
sequence_fields = ['l10n_fr_closing_sequence_id']
|
||||
company._create_secure_sequence(sequence_fields)
|
||||
return res
|
||||
@@ -0,0 +1,8 @@
|
||||
<odoo>
|
||||
<record model="ir.rule" id="account_sale_closing_multi_company">
|
||||
<field name="name">Sale Closing multi-company</field>
|
||||
<field name="model_id" ref="model_account_sale_closing"/>
|
||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_l10n_fr_sale_closing_account_sale_closing_user,l10n_fr_sale_closing.account.sale.closing.user,l10n_fr_sale_closing.model_account_sale_closing,base.group_user,1,0,0,0
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 562 B |
@@ -0,0 +1,68 @@
|
||||
<odoo>
|
||||
<record id="list_view_account_sale_closing" model="ir.ui.view">
|
||||
<field name="name">Sales Closings</field>
|
||||
<field name="model">account.sale.closing</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="false" import="false">
|
||||
<field name="date_closing_start"/>
|
||||
<field name="date_closing_stop"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="frequency"/>
|
||||
<field name="sequence_number" groups="base.group_no_one"/>
|
||||
<field name="total_interval"/>
|
||||
<field name="cumulative_total"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="form_view_account_sale_closing" model="ir.ui.view">
|
||||
<field name="name">Sales Closings</field>
|
||||
<field name="model">account.sale.closing</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false" edit="false" string="Account Closing">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date_closing_start"/>
|
||||
<field name="date_closing_stop"/>
|
||||
<field name="frequency"/>
|
||||
<field name="sequence_number" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="total_interval"/>
|
||||
<field name="cumulative_total"/>
|
||||
<field name="last_move_id" groups="account.group_account_user"/>
|
||||
<field name="last_move_hash" groups="account.group_account_user"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_list_view_account_sale_closing" model="ir.actions.act_window">
|
||||
<field name="name">Sales Closings</field>
|
||||
<field name="res_model">account.sale.closing</field>
|
||||
<field name="view_type">tree</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_nocreate">
|
||||
The closings are created by Odoo
|
||||
</p><p>
|
||||
Sales closings run automatically on a daily, monthly and annual basis. It computes both period and cumulative totals from all the sales entries posted in the system after the previous closing.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_list_view_account_sale_closing" id="menu_account_closing_reporting" parent="l10n_fr.account_reports_fr_statements_menu" sequence="90"/>
|
||||
</odoo>
|
||||
@@ -160,7 +160,7 @@ or (contract.wage * 0.10))</field>
|
||||
<field name="name">Arrears</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.ARS.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.ARS and inputs.ARS.amount</field>
|
||||
<field eval="0.0" name="amount_fix"/>
|
||||
<field name="sequence" eval="28"/>
|
||||
<field name="note">Generally arrears are fully taxable, but employee may claim exemption u/s 89(1).
|
||||
@@ -185,7 +185,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.LE.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.LE and inputs.LE.amount</field>
|
||||
<field name="sequence" eval="30"/>
|
||||
<field name="note">Payment by way of leave encashment received by Central and State Govt.employees at the time of retirement in respect of the period of earned leave at credit is fully exempt. In case of other employees, the exemption is to be limited to minimum of all below:
|
||||
1.The actual amount received
|
||||
@@ -199,7 +199,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Performance Incentive</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.PERF.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.PERF and inputs.PERF.amount</field>
|
||||
<field name="sequence" eval="31"/>
|
||||
<field name="note">This would be fully taxable based on incentive.</field>
|
||||
</record>
|
||||
@@ -209,7 +209,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Bonus</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.BNS.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.BNS and inputs.BNS.amount</field>
|
||||
<field name="sequence" eval="41"/>
|
||||
<field name="note">This would be fully taxable based on Bonus.</field>
|
||||
</record>
|
||||
@@ -229,7 +229,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Medical Reimbursement</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.MR.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.MR and inputs.MR.amount</field>
|
||||
<field name="sequence" eval="32"/>
|
||||
<field name="note">This component is on-taxable up to 15000 per year (or Rs 1250 per month) on producing medical bills.</field>
|
||||
</record>
|
||||
@@ -250,7 +250,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Periodical Journals</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.PJ.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.PJ and inputs.PJ.amount</field>
|
||||
<field name="sequence" eval="34"/>
|
||||
<field name="note">Some employers may provide component for buying magazines, journals and books as a part of knowledge enhancement for business growth.This part would become non taxable on providing original bills.</field>
|
||||
</record>
|
||||
@@ -280,7 +280,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Telephone Reimbursement</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.TR.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.TR and inputs.TR.amount</field>
|
||||
<field name="sequence" eval="36"/>
|
||||
<field name="note">In some of the cases, companies may provide a component for telephone bills.Employees may provide actual phone usage bills to reimburse this component and make it non-taxable.
|
||||
</field>
|
||||
@@ -300,7 +300,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Car Expenses Reimbursement</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.CEI.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.CEI and inputs.CEI.amount</field>
|
||||
<field name="sequence" eval="38"/>
|
||||
<field name="note">In case company provides component for this and employee use self owned car for official and personal purposes, Rs 1800 per month would be non-taxable on showing bills for fuel or can maintenance. This amount would be Rs 2400 in case car is more capacity than 1600cc.</field>
|
||||
</record>
|
||||
@@ -310,7 +310,7 @@ Now difference of income tax between payment year and actual year would be allow
|
||||
<field name="name">Internet Expense</field>
|
||||
<field name="category_id" ref="hr_payroll.ALW"/>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = inputs.IE.amount</field>
|
||||
<field name="amount_python_compute">result = inputs.IE and inputs.IE.amount</field>
|
||||
<field name="sequence" eval="39"/>
|
||||
<field name="note">Employer may also provide reimbursement of internet expenses and thus this would become non taxable.</field>
|
||||
</record>
|
||||
@@ -463,7 +463,7 @@ or - (contract.wage * contract.voluntary_provident_fund / 100))</field>
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">result = - (inputs.LAI.amount)</field>
|
||||
<field name="amount_python_compute">result = inputs.LAI and - (inputs.LAI.amount)</field>
|
||||
<field name="register_id" ref="hr_leave_availed_register"/>
|
||||
<field name="sequence" eval="180"/>
|
||||
</record>
|
||||
|
||||
@@ -179,20 +179,17 @@ tag_lu_177,I.B.5. Tabacs fabriqués,taxes
|
||||
tag_lu_178,Base - Vente Biens Tabacs - Extracommunautaire,taxes
|
||||
tag_lu_179,Base - Vente Biens Tabacs - Intracommunautaire,taxes
|
||||
tag_lu_180,Base - Vente Biens Tabacs - Pays,taxes
|
||||
tag_lu_181,I.B.6.a) Livraisons subséquentes à des acquisitions intracommunautaires dans
|
||||
le cadre d'opérations triangulaires, lorsque le destinataire identifié à la TVA dans l'Etat membre de destination des biens y est le redevable de la taxe (4),taxes
|
||||
tag_lu_181,"I.B.6.a) Livraisons subséquentes à des acquisitions intracommunautaires dans le cadre d'opérations triangulaires, lorsque le destinataire identifié à la TVA dans l'Etat membre de destination des biens y est le redevable de la taxe (4)",taxes
|
||||
tag_lu_182,Base - Vente Biens – Triangulaire Intracommunautaire,taxes
|
||||
tag_lu_183,I.B.6.d) Autres opérations réalisées (imposables) à l'étranger,taxes
|
||||
tag_lu_184,Base - Vente Prestations - Extracommunautaire,taxes
|
||||
tag_lu_185,I.B.6.c) Opérations réalisées dans le cadre du régime particulier de l'article 56sexies,taxes
|
||||
tag_lu_186,I.B.7. Opérations à l'intérieur du pays pour lesquelles le preneur est le redevable,taxes
|
||||
tag_lu_187,I.B.6.b)1) Prestations de services à des identifiés à la TVA dans un autre
|
||||
Etat membre non exonérées dans l'Etat membre du preneur redevable (art.17/1/b) (5),taxes
|
||||
tag_lu_187,I.B.6.b)1) Prestations de services à des identifiés à la TVA dans un autre Etat membre non exonérées dans l'Etat membre du preneur redevable (art.17/1/b) (5),taxes
|
||||
tag_lu_188,Base - Vente Prestations - Intracommunautaire,taxes
|
||||
tag_lu_189,I.B.6.b)2) Prestations de services à des identifiés à la TVA dans un autre
|
||||
Etat membre exonérées dans l'Etat membre du preneur (art.17/1/b),taxes
|
||||
tag_lu_189,I.B.6.b)2) Prestations de services à des identifiés à la TVA dans un autre Etat membre exonérées dans l'Etat membre du preneur (art.17/1/b),taxes
|
||||
tag_lu_190,Base - Vente Prestations - Intracommunautaire exonérées dans l'état membre,taxes
|
||||
tag_lu_191,I.B.1. Livraisons intracommunautaires de biens à des personnes identifiées à la TVA dans unautre Etat membre (art.43/1/d e et f) (3),taxes
|
||||
tag_lu_191,I.B.1. Livraisons intracommunautaires de biens à des personnes identifiées à la TVA dans un autre Etat membre (art.43/1/d e et f) (3),taxes
|
||||
tag_lu_192,Base - Vente Biens - Intracommunautaire,taxes
|
||||
tag_lu_193,I.C. Chiffre d'affaires imposable,taxes
|
||||
tag_lu_194,II.A. Ventilation du chiffre d'affaires imposable – base,taxes
|
||||
@@ -598,4 +595,4 @@ tag_lu_594,Taxe - Investissement Prestations 17% - Intracommunautaire (-),taxes
|
||||
tag_lu_595,Taxe - Investissement Prestations 3% - Intracommunautaire (-),taxes
|
||||
tag_lu_596,Taxe - Investissement Prestations 6% - Intracommunautaire (-),taxes
|
||||
tag_lu_597,Taxe - Investissement Prestations 8% - Intracommunautaire (-),taxes
|
||||
tag_lu_598,Base - Sans Taxe,taxes
|
||||
tag_lu_598,Base - Sans Taxe,taxes
|
||||
|
||||
|
@@ -15,6 +15,12 @@ class IrModel(models.Model):
|
||||
help="Whether this model supports messages and notifications.",
|
||||
)
|
||||
|
||||
def unlink(self):
|
||||
# Delete followers for models that will be unlinked.
|
||||
query = "DELETE FROM mail_followers WHERE res_model IN %s"
|
||||
self.env.cr.execute(query, [tuple(self.mapped('model'))])
|
||||
return super(IrModel, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if self and 'is_mail_thread' in vals:
|
||||
|
||||
@@ -846,6 +846,9 @@ class Message(models.Model):
|
||||
('channel_ids', 'in', email_channels.ids),
|
||||
('email', '!=', self_sudo.author_id.email or self_sudo.email_from),
|
||||
])._notify(self, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature)
|
||||
|
||||
notif_partners._notify_by_chat(self)
|
||||
|
||||
channels_sudo._notify(self)
|
||||
|
||||
# Discard cache, because child / parent allow reading and therefore
|
||||
|
||||
@@ -2150,7 +2150,7 @@ class MailThread(models.AbstractModel):
|
||||
new_channels.setdefault(header_follower.channel_id.id, set()).add(new_subtype.id)
|
||||
|
||||
# add followers coming from res.users relational fields that are tracked
|
||||
to_add_users = self.env['res.users'].sudo().browse([values[name] for name in user_field_lst if values.get(name)])
|
||||
to_add_users = self.env['res.users'].sudo().browse([values[name] for name in user_field_lst if values.get(name)]).filtered(lambda u: u.partner_id.active)
|
||||
for partner in to_add_users.mapped('partner_id'):
|
||||
new_partners.setdefault(partner.id, None)
|
||||
|
||||
|
||||
@@ -127,6 +127,21 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({
|
||||
return _super.apply(self, arguments);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Override for res.partner
|
||||
* name_get is dynamic (based on context) while display_name is static
|
||||
* (stored)
|
||||
*/
|
||||
get_render_data: function(ids){
|
||||
this.dataset.cancel_read();
|
||||
return this.dataset.name_get(ids).then(function (names) {
|
||||
return _.map(names, function(name) {
|
||||
return {id: name[0], display_name: name[1]};
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
field_registry.add('many2many_tags_email', FieldMany2ManyTagsEmail);
|
||||
|
||||
@@ -161,7 +161,7 @@ class MrpProduction(models.Model):
|
||||
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
|
||||
priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
|
||||
readonly=True, states={'confirmed': [('readonly', False)]}, default='1')
|
||||
is_locked = fields.Boolean('Is Locked', default=True)
|
||||
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
|
||||
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
|
||||
production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production')
|
||||
|
||||
@@ -216,8 +216,9 @@ class MrpProduction(models.Model):
|
||||
if order.bom_id.ready_to_produce == 'all_available':
|
||||
order.availability = any(move.state not in ('assigned', 'done', 'cancel') for move in order.move_raw_ids) and 'waiting' or 'assigned'
|
||||
else:
|
||||
partial_list = [x.state in ('partially_available', 'assigned') for x in order.move_raw_ids]
|
||||
assigned_list = [x.state in ('assigned', 'done', 'cancel') for x in order.move_raw_ids]
|
||||
move_raw_ids = order.move_raw_ids.filtered(lambda m: m.product_qty)
|
||||
partial_list = [x.state in ('partially_available', 'assigned') for x in move_raw_ids]
|
||||
assigned_list = [x.state in ('assigned', 'done', 'cancel') for x in move_raw_ids]
|
||||
order.availability = (all(assigned_list) and 'assigned') or (any(partial_list) and 'partially_available') or 'waiting'
|
||||
|
||||
@api.depends('move_raw_ids', 'is_locked', 'state', 'move_raw_ids.quantity_done')
|
||||
|
||||
@@ -22,7 +22,7 @@ class MrpRouting(models.Model):
|
||||
copy=True, oldname='workcenter_lines')
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', 'Production Location',
|
||||
help="Keep empty if you produce at the location where you find the raw materials."
|
||||
help="Keep empty if you produce at the location where you find the raw materials. "
|
||||
"Set a location if you produce at a fixed location. This can be a partner location "
|
||||
"if you subcontract the manufacturing operations.")
|
||||
company_id = fields.Many2one(
|
||||
@@ -49,8 +49,8 @@ class MrpRoutingWorkcenter(models.Model):
|
||||
routing_id = fields.Many2one(
|
||||
'mrp.routing', 'Parent Routing',
|
||||
index=True, ondelete='cascade', required=True,
|
||||
help="The routing contains all the Work Centers used and for how long. This will create work orders afterwards"
|
||||
"which alters the execution of the manufacturing order. ")
|
||||
help="The routing contains all the Work Centers used and for how long. This will create work orders afterwards "
|
||||
"which alters the execution of the manufacturing order.")
|
||||
note = fields.Text('Description')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
|
||||
@@ -389,9 +389,10 @@ class MrpWorkorder(models.Model):
|
||||
self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding)
|
||||
self._generate_lot_ids()
|
||||
|
||||
if self.next_work_order_id and self.production_id.product_id.tracking != 'none':
|
||||
self.next_work_order_id._assign_default_final_lot_id()
|
||||
|
||||
if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0:
|
||||
if self.next_work_order_id and self.production_id.product_id.tracking != 'none':
|
||||
self.next_work_order_id._assign_default_final_lot_id()
|
||||
self.button_finish()
|
||||
return True
|
||||
|
||||
@@ -517,4 +518,4 @@ class MrpWorkorder(models.Model):
|
||||
@api.depends('qty_production', 'qty_produced')
|
||||
def _compute_qty_remaining(self):
|
||||
for wo in self:
|
||||
wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id)
|
||||
wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
|
||||
|
||||
@@ -190,12 +190,18 @@ class StockMove(models.Model):
|
||||
|
||||
def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False):
|
||||
if lot:
|
||||
ml = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
|
||||
move_lines = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
|
||||
else:
|
||||
ml = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id)
|
||||
if ml:
|
||||
new_quantity_done = (ml.qty_done + qty_to_add)
|
||||
if new_quantity_done >= ml.product_uom_qty:
|
||||
move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id)
|
||||
for ml in move_lines:
|
||||
rounding = ml.product_uom_id.rounding
|
||||
if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0:
|
||||
break
|
||||
quantity_to_process = min(qty_to_add, ml.product_uom_qty - ml.qty_done)
|
||||
qty_to_add -= quantity_to_process
|
||||
|
||||
new_quantity_done = (ml.qty_done + quantity_to_process)
|
||||
if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0:
|
||||
ml.write({'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id})
|
||||
else:
|
||||
new_qty_reserved = ml.product_uom_qty - new_quantity_done
|
||||
@@ -204,7 +210,8 @@ class StockMove(models.Model):
|
||||
'lot_produced_id': final_lot.id}
|
||||
ml.copy(default=default)
|
||||
ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_reserved, 'qty_done': 0})
|
||||
else:
|
||||
|
||||
if float_compare(qty_to_add, 0, precision_rounding=self.product_uom.rounding) > 0:
|
||||
vals = {
|
||||
'move_id': self.id,
|
||||
'product_id': self.product_id.id,
|
||||
|
||||
@@ -276,7 +276,7 @@ class TestMrpOrder(TestMrpCommon):
|
||||
'final_lot_id': finished_lot.id,
|
||||
'qty_producing': 48
|
||||
})
|
||||
|
||||
|
||||
kit_wo.record_production()
|
||||
|
||||
self.assertEqual(kit_wo.state, 'done', "Workorder should be in done state.")
|
||||
@@ -431,9 +431,9 @@ class TestMrpOrder(TestMrpCommon):
|
||||
""" In previous versions we had rounding and efficiency fields. We check if we can still do the same, but with only the rounding on the UoM"""
|
||||
self.product_6.uom_id.rounding = 1.0
|
||||
bom_eff = self.env['mrp.bom'].create({'product_id': self.product_6.id,
|
||||
'product_tmpl_id': self.product_6.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': self.product_6.uom_id.id,
|
||||
'product_tmpl_id': self.product_6.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': self.product_6.uom_id.id,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}),
|
||||
@@ -447,7 +447,7 @@ class TestMrpOrder(TestMrpCommon):
|
||||
#Check the production order has the right quantities
|
||||
self.assertEqual(production.move_raw_ids[0].product_qty, 41, 'The quantity should be rounded up')
|
||||
self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up')
|
||||
|
||||
|
||||
# produce product
|
||||
produce_wizard = self.env['mrp.product.produce'].with_context({
|
||||
'active_id': production.id,
|
||||
@@ -516,3 +516,65 @@ class TestMrpOrder(TestMrpCommon):
|
||||
}).create({})
|
||||
self.assertEqual(len(product_produce.produce_line_ids), 1, 'You should have 1 produce lines since one has already be consumed.')
|
||||
self.assertEqual(product_produce.produce_line_ids[0].lot_id, remaining_lot, 'Wrong lot proposed.')
|
||||
|
||||
def test_product_produce_3(self):
|
||||
""" Check that line are created when the consumed products are
|
||||
tracked by serial and the lot proposed are correct. """
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.stock_shelf_1 = self.env.ref('stock.stock_location_components')
|
||||
self.stock_shelf_2 = self.env.ref('stock.stock_location_14')
|
||||
mo, _, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot', qty_base_1=10, qty_final=1)
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
first_lot_for_p1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p1.id,
|
||||
})
|
||||
second_lot_for_p1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot2',
|
||||
'product_id': p1.id,
|
||||
})
|
||||
|
||||
final_product_lot = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p_final.id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 3, lot_id=first_lot_for_p1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 3, lot_id=first_lot_for_p1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 8, lot_id=second_lot_for_p1)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
|
||||
mo.action_assign()
|
||||
product_produce = self.env['mrp.product.produce'].with_context({
|
||||
'active_id': mo.id,
|
||||
'active_ids': [mo.id],
|
||||
}).create({
|
||||
'product_qty': 1.0,
|
||||
'lot_id': final_product_lot.id,
|
||||
})
|
||||
# product 1 lot 1 shelf1
|
||||
# product 1 lot 1 shelf2
|
||||
# product 1 lot 2
|
||||
self.assertEqual(len(product_produce.produce_line_ids), 3, 'You should have 3 produce lines. lot 1 shelf_1, lot 1 shelf_2, lot2')
|
||||
|
||||
for produce_line in product_produce.produce_line_ids:
|
||||
produce_line.qty_done = produce_line.qty_to_consume + 1
|
||||
product_produce.do_produce()
|
||||
|
||||
move_1 = mo.move_raw_ids.filtered(lambda m: m.product_id == p1)
|
||||
# qty_done/product_uom_qty lot
|
||||
# 3/3 lot 1 shelf 1
|
||||
# 1/1 lot 1 shelf 2
|
||||
# 2/2 lot 1 shelf 2
|
||||
# 2/0 lot 1 other
|
||||
# 5/4 lot 2
|
||||
ml_to_shelf_1 = move_1.move_line_ids.filtered(lambda ml: ml.lot_id == first_lot_for_p1 and ml.location_id == self.stock_shelf_1)
|
||||
ml_to_shelf_2 = move_1.move_line_ids.filtered(lambda ml: ml.lot_id == first_lot_for_p1 and ml.location_id == self.stock_shelf_2)
|
||||
|
||||
self.assertEqual(sum(ml_to_shelf_1.mapped('qty_done')), 3.0, '3 units should be took from shelf1 as reserved.')
|
||||
self.assertEqual(sum(ml_to_shelf_2.mapped('qty_done')), 3.0, '3 units should be took from shelf2 as reserved.')
|
||||
self.assertEqual(move_1.quantity_done, 13, 'You should have used the tem units.')
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done', "Production order should be in done state.")
|
||||
|
||||
@@ -112,7 +112,6 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
self.assertEqual(product_table_leg.qty_available, 19)
|
||||
self.assertEqual(product_table_sheet.qty_available, 19)
|
||||
|
||||
@unittest.skip('failing test: is up to date?')
|
||||
def test_00b_workorder_process(self):
|
||||
""" Testing consume quants and produced quants with workorder """
|
||||
dining_table = self.env.ref("mrp.product_product_computer_desk")
|
||||
@@ -120,11 +119,18 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
product_table_leg = self.env.ref('mrp.product_product_computer_desk_leg')
|
||||
product_bolt = self.env.ref('mrp.product_product_computer_desk_bolt')
|
||||
|
||||
bom = self.env['mrp.bom'].browse(self.ref("mrp.mrp_bom_desk"))
|
||||
bom.routing_id = self.ref('mrp.mrp_routing_1')
|
||||
|
||||
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.routing_id.operation_ids[0]
|
||||
bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.routing_id.operation_ids[1]
|
||||
bom.bom_line_ids.filtered(lambda p: p.product_id == product_bolt).operation_id = bom.routing_id.operation_ids[2]
|
||||
|
||||
production_table = self.env['mrp.production'].create({
|
||||
'product_id': dining_table.id,
|
||||
'product_qty': 2.0,
|
||||
'product_uom_id': dining_table.uom_id.id,
|
||||
'bom_id': self.ref("mrp.mrp_bom_desk")
|
||||
'bom_id': bom.id,
|
||||
})
|
||||
|
||||
# Set tracking lot on finish and consume products.
|
||||
@@ -185,11 +191,11 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
finished_lot = self.env['stock.production.lot'].create({'product_id': production_table.product_id.id})
|
||||
workorders[0].write({'final_lot_id': finished_lot.id, 'qty_producing': 1.0})
|
||||
workorders[0].button_start()
|
||||
workorders[0].active_move_lot_ids[0].write({'lot_id': lot_sheet.id, 'quantity_done': 1})
|
||||
workorders[0].active_move_line_ids[0].write({'lot_id': lot_sheet.id, 'qty_done': 1})
|
||||
self.assertEqual(workorders[0].state, 'progress')
|
||||
workorders[0].record_production()
|
||||
|
||||
move_table_sheet = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_sheet)
|
||||
move_table_sheet = production_table.move_raw_ids.filtered(lambda p: p.product_id == product_table_sheet)
|
||||
self.assertEqual(move_table_sheet.quantity_done, 1)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
@@ -197,9 +203,9 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
# ---------------------------------------------------------
|
||||
workorders[1].button_start()
|
||||
workorders[1].qty_producing = 1.0
|
||||
workorders[1].active_move_lot_ids[0].write({'lot_id': lot_leg.id, 'quantity_done': 4})
|
||||
workorders[1].active_move_line_ids[0].write({'lot_id': lot_leg.id, 'qty_done': 4})
|
||||
workorders[1].record_production()
|
||||
move_leg = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_leg)
|
||||
move_leg = production_table.move_raw_ids.filtered(lambda p: p.product_id == product_table_leg)
|
||||
#self.assertEqual(workorders[1].state, 'done')
|
||||
self.assertEqual(move_leg.quantity_done, 4)
|
||||
|
||||
@@ -208,9 +214,9 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
# ---------------------------------------------------------
|
||||
workorders[2].button_start()
|
||||
workorders[2].qty_producing = 1.0
|
||||
move_lot = workorders[2].active_move_lot_ids[0]
|
||||
move_lot.write({'lot_id': lot_bolt.id, 'quantity_done': 4})
|
||||
move_table_bolt = production_table.move_raw_ids.filtered(lambda x : x.product_id.id == product_bolt.id)
|
||||
move_lot = workorders[2].active_move_line_ids[0]
|
||||
move_lot.write({'lot_id': lot_bolt.id, 'qty_done': 4})
|
||||
move_table_bolt = production_table.move_raw_ids.filtered(lambda p: p.product_id.id == product_bolt.id)
|
||||
workorders[2].record_production()
|
||||
self.assertEqual(move_table_bolt.quantity_done, 4)
|
||||
|
||||
@@ -222,9 +228,9 @@ class TestWorkOrderProcess(common.TransactionCase):
|
||||
# Check consume quants and produce quants after posting inventory
|
||||
# ---------------------------------------------------------------
|
||||
production_table.post_inventory()
|
||||
self.assertEqual(sum(move_table_sheet.quant_ids.mapped('qty')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name)
|
||||
self.assertEqual(sum(move_leg.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name)
|
||||
self.assertEqual(sum(move_table_bolt.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name)
|
||||
self.assertEqual(sum(move_table_sheet.mapped('quantity_done')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name)
|
||||
self.assertEqual(sum(move_leg.mapped('quantity_done')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name)
|
||||
self.assertEqual(sum(move_table_bolt.mapped('quantity_done')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name)
|
||||
|
||||
def test_01_without_workorder(self):
|
||||
""" Testing consume quants and produced quants without workorder """
|
||||
|
||||
@@ -125,9 +125,11 @@
|
||||
<field name="name">Work Orders Performance</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.workorder</field>
|
||||
<field name="domain">[('state','=','done')]</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{'search_default_done': True}</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">graph,pivot,tree,form,gantt</field>
|
||||
<field name="search_view_id" ref="view_mrp_production_work_order_search"/>
|
||||
</record>
|
||||
|
||||
<!-- Workcenter Kanban view-->
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
<tree>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="duration" widget="float_time"/>
|
||||
<field name="duration" widget="float_time" sum="Total duration"/>
|
||||
<field name="user_id"/>
|
||||
<field name="loss_id" string="Efficiency"/>
|
||||
</tree>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='description']" position="attributes">
|
||||
<attribute name="attrs">{'invisible': [('use_pad', '=', True)]}</attribute>
|
||||
<attribute name="attrs">{'invisible': [('use_pad', '=', True)], 'readonly': [('use_pad', '=', True)]}</attribute>
|
||||
</xpath>
|
||||
<field name="description" position="after">
|
||||
<field name="use_pad" invisible="1"/>
|
||||
|
||||
@@ -121,7 +121,7 @@ class PaymentAcquirer(models.Model):
|
||||
('ask', 'Let the customer decide'),
|
||||
('always', 'Always')],
|
||||
string='Save Cards', default='none',
|
||||
help="This option allows customers to save their credit card as a payment token and to reuse it for a later purchase."
|
||||
help="This option allows customers to save their credit card as a payment token and to reuse it for a later purchase. "
|
||||
"If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you "
|
||||
"issue an invoice.")
|
||||
token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support', search='_search_is_tokenized')
|
||||
@@ -418,14 +418,9 @@ class PaymentAcquirer(models.Model):
|
||||
# TDE FIXME: remove that brol
|
||||
if self.module_id and self.module_state != 'installed':
|
||||
self.module_id.button_immediate_install()
|
||||
context = dict(self._context, active_id=self.ids[0])
|
||||
return {
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'payment.acquirer',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': self.ids[0],
|
||||
'context': context,
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
|
||||
class PaymentIcon(models.Model):
|
||||
|
||||
@@ -132,7 +132,7 @@ odoo.define('payment.payment_form', function (require) {
|
||||
// if there's a prepare tx url set
|
||||
if ($tx_url.length === 1) {
|
||||
// if the user wants to save his credit card info
|
||||
var form_save_token = $('input[name="o_payment_form_save_token"]').checked === true;
|
||||
var form_save_token = $('input[name="o_payment_form_save_token"]').prop('checked');
|
||||
// then we call the route to prepare the transaction
|
||||
ajax.jsonRpc($tx_url[0].value, 'call', {
|
||||
'acquirer_id': parseInt(acquirer_id),
|
||||
@@ -410,14 +410,18 @@ odoo.define('payment.payment_form', function (require) {
|
||||
},
|
||||
});
|
||||
|
||||
require('web.dom_ready'); // only start this when dom is ready
|
||||
if (!$('.o_payment_form').length) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.o_payment_form'");
|
||||
}
|
||||
$('.o_payment_form').each(function () {
|
||||
var $elem = $(this);
|
||||
var form = new PaymentForm(null, $elem.data());
|
||||
form.attachTo($elem);
|
||||
$(function () {
|
||||
// TODO move this to another module, requiring dom_ready and rejecting
|
||||
// the returned deferred to get the proper message
|
||||
if (!$('.o_payment_form').length) {
|
||||
console.log("DOM doesn't contain '.o_payment_form'");
|
||||
return;
|
||||
}
|
||||
$('.o_payment_form').each(function () {
|
||||
var $elem = $(this);
|
||||
var form = new PaymentForm(null, $elem.data());
|
||||
form.attachTo($elem);
|
||||
});
|
||||
});
|
||||
|
||||
return PaymentForm;
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
<i class="fa fa-lock"> Pay</i>
|
||||
</t>
|
||||
</button>
|
||||
<button t-if="mode == 'manage'" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<button t-if="mode == 'manage' and s2s_acquirers" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<i class="fa fa-plus-circle"> Add new card</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -165,6 +165,22 @@ class AuthorizeAPI():
|
||||
etree.SubElement(creditCard, "cardCode").text = card_code
|
||||
etree.SubElement(root, "validationMode").text = 'liveMode'
|
||||
response = self._authorize_request(root)
|
||||
|
||||
# If the user didn't set up authorize.net properly then the response
|
||||
# won't contain stuff like customerProfileId and accessing text
|
||||
# will raise a NoneType has no text attribute
|
||||
msg = response.find('messages')
|
||||
if msg is not None:
|
||||
rc = msg.find('resultCode')
|
||||
if rc is not None and rc.text == 'Error':
|
||||
err = msg.find('message')
|
||||
err_code = err.find('code').text
|
||||
err_msg = err.find('text').text
|
||||
raise UserError(
|
||||
"Authorize.net Error:\nCode: %s\nMessage: %s"
|
||||
% (err_code, err_msg)
|
||||
)
|
||||
|
||||
res = dict()
|
||||
res['profile_id'] = response.find('customerProfileId').text
|
||||
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
|
||||
|
||||
@@ -207,7 +207,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
},{
|
||||
model: 'account.tax',
|
||||
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'],
|
||||
domain: null,
|
||||
domain: function(self) {return [['company_id', '=', self.company && self.company.id || false]]},
|
||||
loaded: function(self, taxes){
|
||||
self.taxes = taxes;
|
||||
self.taxes_by_id = {};
|
||||
|
||||
@@ -2030,6 +2030,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
}
|
||||
|
||||
order.initialize_validation_date();
|
||||
order.finalized = true;
|
||||
|
||||
if (order.is_to_invoice()) {
|
||||
var invoiced = this.pos.push_and_invoice_order(order);
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" title="Don’t turn this option on if you take orders on smartphones or tablets. Such devices already benefit from a native keyboard.">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" title="Don't turn this option on if you take orders on smartphones or tablets. Such devices already benefit from a native keyboard.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="iface_vkeyboard"/>
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,7 @@ class Pricelist(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not date:
|
||||
date = self._context.get('date') or fields.Date.today()
|
||||
date = self._context.get('date') or fields.Date.context_today(self)
|
||||
if not uom_id and self._context.get('uom'):
|
||||
uom_id = self._context['uom']
|
||||
if uom_id:
|
||||
|
||||
@@ -40,6 +40,7 @@ class Company(models.Model):
|
||||
'name': _('Leaves'),
|
||||
'project_id': company.leave_timesheet_project_id.id,
|
||||
'active': False,
|
||||
'company_id': False,
|
||||
})
|
||||
company.write({
|
||||
'leave_timesheet_task_id': task.id,
|
||||
|
||||
@@ -15,7 +15,7 @@ class CustomerPortal(CustomerPortal):
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(CustomerPortal, self)._prepare_portal_layout_values()
|
||||
partner = request.env.user.partner_id
|
||||
values['purchase_count'] = request.env['purchase.order'].search_count([
|
||||
values['purchase_count'] = request.env['purchase.order'].sudo().search_count([
|
||||
'|',
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('partner_id', 'child_of', [partner.commercial_partner_id.id]),
|
||||
|
||||
@@ -167,7 +167,7 @@ class TestStockValuation(TransactionCase):
|
||||
move1 = picking1.move_lines[0]
|
||||
|
||||
# convert the price unit in the company currency
|
||||
price_unit_usd = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=False)
|
||||
price_unit_usd = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=True)
|
||||
|
||||
# the unit price of the move is the unit price of the purchase order line converted in
|
||||
# the company's currency
|
||||
@@ -181,7 +181,7 @@ class TestStockValuation(TransactionCase):
|
||||
'company_id': po1.company_id.id,
|
||||
})
|
||||
eur_currency._compute_current_rate()
|
||||
price_unit_usd_new_rate = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=False)
|
||||
price_unit_usd_new_rate = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=True)
|
||||
|
||||
# the new price_unit is lower than th initial because of the rate's change
|
||||
self.assertLess(price_unit_usd_new_rate, price_unit_usd)
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="product_id" attrs="{'readonly': [('state', 'in', ('purchase', 'to approve','done', 'cancel'))]}" context="{'partner_id':parent.partner_id, 'quantity':product_qty,'uom':product_uom, 'company_id': parent.company_id}" force_save="1"/>
|
||||
<field name="name"/>
|
||||
<field name="move_dest_ids" invisible="1"/>
|
||||
<field name="date_planned"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
<field name="account_analytic_id" context="{'default_partner_id':parent.partner_id}" groups="purchase.group_analytic_accounting"/>
|
||||
|
||||
@@ -722,7 +722,7 @@ class ResourceCalendarLeaves(models.Model):
|
||||
date_to = fields.Datetime('End Date', required=True)
|
||||
tz = fields.Selection(
|
||||
_tz_get, string='Timezone', default=lambda self: self._context.get('tz', self.env.user.tz or 'UTC'),
|
||||
help="Timezone used when encoding the leave. It is used to correctly"
|
||||
help="Timezone used when encoding the leave. It is used to correctly "
|
||||
"localize leave hours when computing time intervals.")
|
||||
resource_id = fields.Many2one(
|
||||
"resource.resource", 'Resource',
|
||||
|
||||
@@ -76,7 +76,7 @@ class AccountInvoiceLine(models.Model):
|
||||
prod_moves = [m for m in moves if m.product_id.id == product_id]
|
||||
prod_qty_done = factor * qty_done
|
||||
prod_quantity = factor * quantity
|
||||
average_price_unit += self._compute_average_price(prod_qty_done, prod_quantity, prod_moves)
|
||||
average_price_unit += factor * self._compute_average_price(prod_qty_done, prod_quantity, prod_moves)
|
||||
price_unit = average_price_unit or price_unit
|
||||
price_unit = self.product_id.uom_id._compute_price(price_unit, self.uom_id)
|
||||
return price_unit
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<record model="crm.team" id="team_sales_department">
|
||||
<field name="name">Europe</field>
|
||||
<field name="dashboard_graph_group">week</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[(4,ref('sales_team.group_sale_salesman'))]"/>
|
||||
@@ -17,6 +18,7 @@
|
||||
<record model="crm.team" id="crm_team_1">
|
||||
<field name="name">America</field>
|
||||
<field name="dashboard_graph_group">week</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -184,7 +184,7 @@ class Inventory(models.Model):
|
||||
})
|
||||
|
||||
def action_start(self):
|
||||
for inventory in self:
|
||||
for inventory in self.filtered(lambda x: x.state not in ('done','cancel')):
|
||||
vals = {'state': 'confirm', 'date': fields.Datetime.now()}
|
||||
if (inventory.filter != 'partial') and not inventory.line_ids:
|
||||
vals.update({'line_ids': [(0, 0, line_values) for line_values in inventory._get_inventory_lines_values()]})
|
||||
@@ -383,9 +383,9 @@ class InventoryLine(models.Model):
|
||||
('prod_lot_id', '=', values.get('prod_lot_id'))])
|
||||
res = super(InventoryLine, self).create(values)
|
||||
if existings:
|
||||
raise UserError(_("You cannot have two inventory adjustements in state 'in Progress' with the same product"
|
||||
"(%s), same location(%s), same package, same owner and same lot. Please first validate"
|
||||
"the first inventory adjustement with this product before creating another one.") % (res.product_id.name, res.location_id.name))
|
||||
raise UserError(_("You cannot have two inventory adjustements in state 'in Progress' with the same product "
|
||||
"(%s), same location (%s), same package, same owner and same lot. Please first validate "
|
||||
"the first inventory adjustement with this product before creating another one.") % (res.product_id.display_name, res.location_id.name))
|
||||
return res
|
||||
|
||||
@api.constrains('product_id')
|
||||
|
||||
@@ -208,7 +208,7 @@ class PushedFlow(models.Model):
|
||||
('manual', 'Manual Operation'),
|
||||
('transparent', 'Automatic No Step Added')], string='Automatic Move',
|
||||
default='manual', index=True, required=True,
|
||||
help="The 'Manual Operation' value will create a stock move after the current one."
|
||||
help="The 'Manual Operation' value will create a stock move after the current one. "
|
||||
"With 'Automatic No Step Added', the location is replaced in the original move.")
|
||||
propagate = fields.Boolean('Propagate cancel and split', default=True, help='If checked, when the previous move is cancelled or split, the move generated by this move will too')
|
||||
active = fields.Boolean('Active', default=True)
|
||||
|
||||
@@ -115,9 +115,9 @@ class StockMove(models.Model):
|
||||
('make_to_stock', 'Default: Take From Stock'),
|
||||
('make_to_order', 'Advanced: Apply Procurement Rules')], string='Supply Method',
|
||||
default='make_to_stock', required=True,
|
||||
help="By default, the system will take from the stock in the source location and passively wait for availability."
|
||||
help="By default, the system will take from the stock in the source location and passively wait for availability. "
|
||||
"The other possibility allows you to directly create a procurement on the source location (and thus ignore "
|
||||
"its current stock) to gather products. If we want to chain moves and have this one to wait for the previous,"
|
||||
"its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, "
|
||||
"this second option should be chosen.")
|
||||
scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True)
|
||||
scrap_ids = fields.One2many('stock.scrap', 'move_id')
|
||||
|
||||
@@ -473,7 +473,6 @@ class StockMoveLine(models.Model):
|
||||
('owner_id', '=', owner_id.id if owner_id else False),
|
||||
('package_id', '=', package_id.id if package_id else False),
|
||||
('product_qty', '>', 0.0),
|
||||
('qty_done', '=', 0.0),
|
||||
('id', '!=', self.id),
|
||||
]
|
||||
oudated_candidates = self.env['stock.move.line'].search(oudated_move_lines_domain)
|
||||
@@ -487,7 +486,10 @@ class StockMoveLine(models.Model):
|
||||
if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0:
|
||||
quantity -= candidate.product_qty
|
||||
move_to_recompute_state |= candidate.move_id
|
||||
candidate.unlink()
|
||||
if candidate.qty_done:
|
||||
candidate.product_uom_qty = 0.0
|
||||
else:
|
||||
candidate.unlink()
|
||||
else:
|
||||
# split this move line and assign the new part to our extra move
|
||||
quantity_split = float_round(
|
||||
|
||||
@@ -407,7 +407,7 @@ class Picking(models.Model):
|
||||
packages = self.env['stock.quant.package']
|
||||
for ml in picking.move_line_ids:
|
||||
if ml.package_id.id == ml.result_package_id.id:
|
||||
if picking._check_move_lines_map_quant_package(ml.package_id):
|
||||
if picking.state in ('done', 'cancel') or picking._check_move_lines_map_quant_package(ml.package_id):
|
||||
packages |= ml.package_id
|
||||
picking.entire_package_ids = packages
|
||||
picking.entire_package_detail_ids = packages
|
||||
@@ -542,7 +542,19 @@ class Picking(models.Model):
|
||||
# call `_action_assign` on every confirmed move which location_id bypasses the reservation
|
||||
self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production') and picking.state == 'confirmed')\
|
||||
.mapped('move_lines')._action_assign()
|
||||
return True
|
||||
if self.env.context.get('planned_picking') and len(self) == 1:
|
||||
action = self.env.ref('stock.action_picking_form')
|
||||
result = action.read()[0]
|
||||
result['res_id'] = self.id
|
||||
result['context'] = {
|
||||
'search_default_picking_type_id': [self.picking_type_id.id],
|
||||
'default_picking_type_id': self.picking_type_id.id,
|
||||
'contact_display': 'partner_address',
|
||||
'planned_picking': False,
|
||||
}
|
||||
return result
|
||||
else:
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_assign(self):
|
||||
@@ -654,10 +666,8 @@ class Picking(models.Model):
|
||||
|
||||
@api.multi
|
||||
def do_unreserve(self):
|
||||
for move in self:
|
||||
for move_line in move.move_lines:
|
||||
move_line._do_unreserve()
|
||||
self.write({'state': 'confirmed'})
|
||||
for picking in self:
|
||||
picking.move_lines._do_unreserve()
|
||||
|
||||
@api.multi
|
||||
def button_validate(self):
|
||||
|
||||
@@ -714,6 +714,48 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
self.assertEqual(move1.reserved_availability, 1.0)
|
||||
|
||||
def test_availability_4(self):
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 30.0)
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_availability_4',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 15.0,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
|
||||
move2 = self.env['stock.move'].create({
|
||||
'name': 'test_availability_4',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 15.0,
|
||||
})
|
||||
move2._action_confirm()
|
||||
move2._action_assign()
|
||||
|
||||
# set 15 as quantity done for the first and 30 as the second
|
||||
move1.move_line_ids.qty_done = 15
|
||||
move2.move_line_ids.qty_done = 30
|
||||
|
||||
# validate the second, the first should be unreserved
|
||||
move2._action_done()
|
||||
|
||||
self.assertEqual(move1.state, 'confirmed')
|
||||
self.assertEqual(move1.move_line_ids.qty_done, 15)
|
||||
self.assertEqual(move2.state, 'done')
|
||||
|
||||
stock_quants = self.env['stock.quant']._gather(self.product1, self.stock_location)
|
||||
self.assertEqual(len(stock_quants), 0)
|
||||
customer_quants = self.env['stock.quant']._gather(self.product1, self.customer_location)
|
||||
self.assertEqual(customer_quants.quantity, 30)
|
||||
self.assertEqual(customer_quants.reserved_quantity, 0)
|
||||
|
||||
def test_unreserve_1(self):
|
||||
""" Check that unreserving a stock move sets the products reserved as available and
|
||||
set the state back to confirmed.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user