mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 26012023
This commit is contained in:
@@ -71,6 +71,7 @@ _ref_vat = {
|
||||
'sk': 'SK2022749619',
|
||||
'sm': 'SM24165',
|
||||
'tr': 'TR1234567890 (VERGINO) or TR17291716060 (TCKIMLIKNO)', # Levent Karakas @ Eska Yazilim A.S.
|
||||
've': 'V-12345678-1, V123456781, V-12.345.678-1',
|
||||
'xi': 'XI123456782',
|
||||
}
|
||||
|
||||
@@ -500,6 +501,57 @@ class ResPartner(models.Model):
|
||||
res.append(False)
|
||||
return all(res)
|
||||
|
||||
def check_vat_ve(self, vat):
|
||||
# https://tin-check.com/en/venezuela/
|
||||
# https://techdocs.broadcom.com/us/en/symantec-security-software/information-security/data-loss-prevention/15-7/About-content-packs/What-s-included-in-Content-Pack-2021-02/Updated-data-identifiers-in-Content-Pack-2021-02/venezuela-national-identification-number-v115451096-d327e108002-CP2021-02.html
|
||||
# Sources last visited on 2022-12-09
|
||||
|
||||
# VAT format: (kind - 1 letter)(identifier number - 8-digit number)(check digit - 1 digit)
|
||||
vat_regex = re.compile(r"""
|
||||
([vecjpg]) # group 1 - kind
|
||||
(
|
||||
(?P<optional_1>-)? # optional '-' (1)
|
||||
[0-9]{2}
|
||||
(?(optional_1)(?P<optional_2>[.])?) # optional '.' (2) only if (1)
|
||||
[0-9]{3}
|
||||
(?(optional_2)[.]) # mandatory '.' if (2)
|
||||
[0-9]{3}
|
||||
(?(optional_1)-) # mandatory '-' if (1)
|
||||
) # group 2 - identifier number
|
||||
([0-9]{1}) # group X - check digit
|
||||
""", re.VERBOSE | re.IGNORECASE)
|
||||
|
||||
matches = re.fullmatch(vat_regex, vat)
|
||||
if not matches:
|
||||
return False
|
||||
|
||||
kind, identifier_number, *_, check_digit = matches.groups()
|
||||
kind = kind.lower()
|
||||
identifier_number = identifier_number.replace("-", "").replace(".", "")
|
||||
check_digit = int(check_digit)
|
||||
|
||||
if kind == 'v': # Venezuela citizenship
|
||||
kind_digit = 1
|
||||
elif kind == 'e': # Foreigner
|
||||
kind_digit = 2
|
||||
elif kind == 'c' or kind == 'j': # Township/Communal Council or Legal entity
|
||||
kind_digit = 3
|
||||
elif kind == 'p': # Passport
|
||||
kind_digit = 4
|
||||
else: # Government ('g')
|
||||
kind_digit = 5
|
||||
|
||||
# === Checksum validation ===
|
||||
multipliers = [3, 2, 7, 6, 5, 4, 3, 2]
|
||||
checksum = kind_digit * 4
|
||||
checksum += sum(map(lambda n, m: int(n) * m, identifier_number, multipliers))
|
||||
|
||||
checksum_digit = 11 - checksum % 11
|
||||
if checksum_digit > 9:
|
||||
checksum_digit = 0
|
||||
|
||||
return check_digit == checksum_digit
|
||||
|
||||
def check_vat_xi(self, vat):
|
||||
""" Temporary Nothern Ireland VAT validation following Brexit
|
||||
As of January 1st 2021, companies in Northern Ireland have a
|
||||
|
||||
@@ -12,6 +12,7 @@ base.aw,243
|
||||
base.az,541
|
||||
base.bb,204
|
||||
base.bd,321
|
||||
base.be,514
|
||||
base.bf,161
|
||||
base.bg,527
|
||||
base.bi,141
|
||||
@@ -76,6 +77,7 @@ base.it,504
|
||||
base.je,568
|
||||
base.jm,205
|
||||
base.jo,301
|
||||
base.jp,331
|
||||
base.ke,137
|
||||
base.kh,315
|
||||
base.ki,416
|
||||
@@ -93,6 +95,7 @@ base.ly,125
|
||||
base.ma,128
|
||||
base.mc,535
|
||||
base.md,556
|
||||
base.me,561
|
||||
base.mg,120
|
||||
base.mh,164
|
||||
base.ml,133
|
||||
@@ -128,6 +131,8 @@ base.pr,251
|
||||
base.pt,501
|
||||
base.py,222
|
||||
base.qa,312
|
||||
base.ro,519
|
||||
base.rs,546
|
||||
base.rw,142
|
||||
base.sc,156
|
||||
base.sd,123
|
||||
@@ -146,15 +151,18 @@ base.tg,109
|
||||
base.tm,558
|
||||
base.tt,203
|
||||
base.tv,419
|
||||
base.tw,330
|
||||
base.tz,135
|
||||
base.ua,559
|
||||
base.ug,136
|
||||
base.uk,510
|
||||
base.us,225
|
||||
base.uy,223
|
||||
base.uz,560
|
||||
base.vc,234
|
||||
base.ve,201
|
||||
base.vn,325
|
||||
base.vu,415
|
||||
base.za,112
|
||||
base.zm,117
|
||||
base.zw,116
|
||||
base.zw,116
|
||||
|
||||
|
@@ -1501,8 +1501,19 @@ class MrpProduction(models.Model):
|
||||
|
||||
# As we have split the moves before validating them, we need to 'remove' the excess reservation
|
||||
if not close_mo:
|
||||
self.move_raw_ids.filtered(lambda m: not m.additional)._do_unreserve()
|
||||
self.move_raw_ids.filtered(lambda m: not m.additional)._action_assign()
|
||||
raw_moves = self.move_raw_ids.filtered(lambda m: not m.additional)
|
||||
raw_moves._do_unreserve()
|
||||
for sml in raw_moves.move_line_ids:
|
||||
try:
|
||||
q = self.env['stock.quant']._update_reserved_quantity(sml.product_id, sml.location_id, sml.qty_done,
|
||||
lot_id=sml.lot_id, package_id=sml.package_id,
|
||||
owner_id=sml.owner_id, strict=True)
|
||||
reserved_qty = sum([x[1] for x in q])
|
||||
reserved_qty = sml.product_id.uom_id._compute_quantity(reserved_qty, sml.product_uom_id)
|
||||
except UserError:
|
||||
reserved_qty = 0
|
||||
sml.with_context(bypass_reservation_update=True).product_uom_qty = reserved_qty
|
||||
raw_moves._recompute_state()
|
||||
# Confirm only productions with remaining components
|
||||
backorders.filtered(lambda mo: mo.move_raw_ids).action_confirm()
|
||||
backorders.filtered(lambda mo: mo.move_raw_ids).action_assign()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from collections import defaultdict
|
||||
from flectra import fields, models, _, api
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.osv import expression
|
||||
from flectra.tools.float_utils import float_compare, float_is_zero
|
||||
|
||||
|
||||
@@ -14,6 +15,32 @@ class MrpProduction(models.Model):
|
||||
'stock.move.line', string="Detail Component", readonly=False,
|
||||
inverse='_inverse_move_line_raw_ids', compute='_compute_move_line_raw_ids'
|
||||
)
|
||||
incoming_picking = fields.Many2one(related='move_finished_ids.move_dest_ids.picking_id')
|
||||
|
||||
@api.depends('name')
|
||||
def name_get(self):
|
||||
return [
|
||||
(record.id, "%s (%s)" % (record.incoming_picking.name, record.name)) if record.bom_id.type == 'subcontract'
|
||||
else (record.id, record.name) for record in self
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None):
|
||||
args = list(args or [])
|
||||
|
||||
if name == '' and operator == 'ilike':
|
||||
return self._search(args, limit=limit, access_rights_uid=name_get_uid)
|
||||
|
||||
# search through MO
|
||||
domain = [(self._rec_name, operator, name)]
|
||||
|
||||
# search through transfers
|
||||
picking_rec_name = self.env['stock.picking']._rec_name
|
||||
picking_domain = [('bom_id.type', '=', 'subcontract'), ('incoming_picking.%s' % picking_rec_name, operator, name)]
|
||||
domain = expression.OR([domain, picking_domain])
|
||||
|
||||
args = expression.AND([args, domain])
|
||||
return self._search(args, limit=limit, access_rights_uid=name_get_uid)
|
||||
|
||||
@api.depends('move_raw_ids.move_line_ids')
|
||||
def _compute_move_line_raw_ids(self):
|
||||
|
||||
@@ -492,6 +492,27 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
|
||||
def test_mo_name(self):
|
||||
receipt_form = Form(self.env['stock.picking'])
|
||||
receipt_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
receipt_form.partner_id = self.subcontractor_partner1
|
||||
with receipt_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
receipt = receipt_form.save()
|
||||
receipt.action_confirm()
|
||||
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
|
||||
display_name = mo.display_name
|
||||
self.assertIn(receipt.name, display_name, "If subcontracted, the name of a MO should contain the associated receipt name")
|
||||
self.assertIn(mo.name, display_name)
|
||||
|
||||
for key_search in [mo.name, receipt.name]:
|
||||
res = mo.name_search(key_search)
|
||||
self.assertTrue(res, 'When looking for "%s", it should find something' % key_search)
|
||||
self.assertEqual(res[0][0], mo.id, 'When looking for "%s", it should find the MO processed above' % key_search)
|
||||
|
||||
|
||||
class TestSubcontractingTracking(TransactionCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -168,10 +168,24 @@ class PurchaseReport(models.Model):
|
||||
if any(field.split(':')[1].split('(')[0] != 'avg' for field in [avg_days_to_purchase] if field):
|
||||
raise UserError("Value: 'avg_days_to_purchase' should only be used to show an average. If you are seeing this message then it is being accessed incorrectly.")
|
||||
|
||||
if 'price_average:avg' in fields:
|
||||
fields.extend(['aggregated_qty_ordered:array_agg(qty_ordered)'])
|
||||
fields.extend(['aggregated_price_average:array_agg(price_average)'])
|
||||
|
||||
res = []
|
||||
if fields:
|
||||
res = super(PurchaseReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
|
||||
|
||||
if 'price_average:avg' in fields:
|
||||
qties = 'aggregated_qty_ordered'
|
||||
special_field = 'aggregated_price_average'
|
||||
for data in res:
|
||||
if data[special_field] and data[qties]:
|
||||
total_unit_cost = sum(float(value) * float(qty) for value, qty in zip(data[special_field], data[qties]) if qty and value)
|
||||
total_qty_ordered = sum(float(qty) for qty in data[qties] if qty)
|
||||
data['price_average'] = (total_unit_cost / total_qty_ordered) if total_qty_ordered else 0
|
||||
del data[special_field]
|
||||
del data[qties]
|
||||
if not res and avg_days_to_purchase:
|
||||
res = [{}]
|
||||
|
||||
|
||||
@@ -88,3 +88,38 @@ class TestPurchaseOrderReport(AccountTestInvoicingCommon):
|
||||
)
|
||||
self.assertEqual(round(report[0]['delay']), -10, msg="The PO has been confirmed 10 days in advance")
|
||||
self.assertEqual(round(report[0]['delay_pass']), 5, msg="There are 5 days between the order date and the planned date")
|
||||
|
||||
def test_avg_price_calculation(self):
|
||||
"""
|
||||
Check that the average price is calculated based on the quantity ordered in each line
|
||||
|
||||
PO:
|
||||
- 10 unit of product A -> price $50
|
||||
- 1 unit of product A -> price $10
|
||||
Total qty_ordered: 11
|
||||
avergae price: 46.36 = ((10 * 50) + (10 * 1)) / 11
|
||||
"""
|
||||
po = self.env['purchase.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 10.0,
|
||||
'price_unit': 50.0,
|
||||
}),
|
||||
(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_qty': 1.0,
|
||||
'price_unit': 10.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
po.button_confirm()
|
||||
po.flush()
|
||||
report = self.env['purchase.report'].read_group(
|
||||
[('product_id', '=', self.product_a.id)],
|
||||
['qty_ordered', 'price_average:avg'],
|
||||
['product_id'],
|
||||
)
|
||||
self.assertEqual(report[0]['qty_ordered'], 11)
|
||||
self.assertEqual(round(report[0]['price_average'], 2), 46.36)
|
||||
|
||||
@@ -559,6 +559,8 @@ class StockMove(models.Model):
|
||||
# messages according to the state of the stock.move records.
|
||||
receipt_moves_to_reassign = self.env['stock.move']
|
||||
move_to_recompute_state = self.env['stock.move']
|
||||
if 'quantity_done' in vals and any(move.state == 'cancel' for move in self):
|
||||
raise UserError(_('You cannot change a cancelled stock move, create a new line instead.'))
|
||||
if 'product_uom' in vals and any(move.state == 'done' for move in self):
|
||||
raise UserError(_('You cannot change the UoM for a stock move that has been set to \'Done\'.'))
|
||||
if 'product_uom_qty' in vals:
|
||||
|
||||
@@ -115,6 +115,9 @@ var AnimationEffect = Class.extend(mixins.ParentedMixin, {
|
||||
* startEvents is received again)
|
||||
* @param {jQuery|DOMElement} [options.$endTarget=$startTarget]
|
||||
* the element(s) on which the endEvents are listened
|
||||
* @param {boolean} [options.enableInModal]
|
||||
* when it is true, it means that the 'scroll' event must be
|
||||
* triggered when scrolling a modal.
|
||||
*/
|
||||
init: function (parent, updateCallback, startEvents, $startTarget, options) {
|
||||
mixins.ParentedMixin.init.call(this);
|
||||
@@ -126,7 +129,8 @@ var AnimationEffect = Class.extend(mixins.ParentedMixin, {
|
||||
// Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks
|
||||
this._updateCallback = updateCallback;
|
||||
this.startEvents = startEvents || 'scroll';
|
||||
const mainScrollingElement = $().getScrollingElement()[0];
|
||||
const modalEl = options.enableInModal ? parent.target.closest('.modal') : null;
|
||||
const mainScrollingElement = modalEl ? modalEl : $().getScrollingElement()[0];
|
||||
const mainScrollingTarget = mainScrollingElement === document.documentElement ? window : mainScrollingElement;
|
||||
this.$startTarget = $($startTarget ? $startTarget : this.startEvents === 'scroll' ? mainScrollingTarget : window);
|
||||
if (options.getStateCallback) {
|
||||
@@ -400,6 +404,7 @@ var Animation = publicWidget.Widget.extend({
|
||||
endEvents: desc.endEvents || undefined,
|
||||
$endTarget: _findTarget(desc.endTarget),
|
||||
maxFPS: self.maxFPS,
|
||||
enableInModal: desc.enableInModal || undefined,
|
||||
});
|
||||
|
||||
// Return the DOM element matching the selector in the form
|
||||
@@ -506,6 +511,7 @@ registry.Parallax = Animation.extend({
|
||||
effects: [{
|
||||
startEvents: 'scroll',
|
||||
update: '_onWindowScroll',
|
||||
enableInModal: true,
|
||||
}],
|
||||
|
||||
/**
|
||||
@@ -514,6 +520,13 @@ registry.Parallax = Animation.extend({
|
||||
start: function () {
|
||||
this._rebuild();
|
||||
$(window).on('resize.animation_parallax', _.debounce(this._rebuild.bind(this), 500));
|
||||
this.modalEl = this.$target[0].closest('.modal');
|
||||
if (this.modalEl) {
|
||||
$(this.modalEl).on('shown.bs.modal.animation_parallax', () => {
|
||||
this._rebuild();
|
||||
this.modalEl.dispatchEvent(new Event('scroll'));
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
@@ -522,6 +535,9 @@ registry.Parallax = Animation.extend({
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
$(window).off('.animation_parallax');
|
||||
if (this.modalEl) {
|
||||
$(this.modalEl).off('.animation_parallax');
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -264,4 +264,28 @@ WysiwygTranslate.include({
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.Parallax.include({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async _computeVisibility() {
|
||||
// Hides parallax options for snippets that are inside a "Newsletter"
|
||||
// popup because the parallax effect is not working in this case. This
|
||||
// is due to the scrollbar which is on the "modal-body" element and not
|
||||
// on the "modal" element.
|
||||
// TODO in master: Make sure that the scrollbar is in the same place as
|
||||
// for the "s_popup" snippet for the "Newsletter" popup and make the
|
||||
// parallax effect work.
|
||||
if (this.$target[0].closest('.o_newsletter_popup')) {
|
||||
return false;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1529,6 +1529,10 @@
|
||||
<t t-set="back_button_link" t-value="'/shop/cart'"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="not (acquirers or tokens)" class="alert alert-warning">
|
||||
<strong>No suitable payment option could be found.</strong><br/>
|
||||
If you believe that it is an error, please contact the website administrator.
|
||||
</div>
|
||||
|
||||
<div t-if="not acquirers" class="mt-2">
|
||||
<a role="button" class="btn-link"
|
||||
|
||||
Reference in New Issue
Block a user