[PATCH] Upstream patch - 26012023

This commit is contained in:
Parthiv Patel
2023-01-26 08:35:56 +00:00
parent 4760f4c1ae
commit 0ebdc7f430
11 changed files with 218 additions and 4 deletions

View File

@@ -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

View File

@@ -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
1 id l10n_cl_customs_code
12 base.az 541
13 base.bb 204
14 base.bd 321
15 base.be 514
16 base.bf 161
17 base.bg 527
18 base.bi 141
77 base.je 568
78 base.jm 205
79 base.jo 301
80 base.jp 331
81 base.ke 137
82 base.kh 315
83 base.ki 416
95 base.ma 128
96 base.mc 535
97 base.md 556
98 base.me 561
99 base.mg 120
100 base.mh 164
101 base.ml 133
131 base.pt 501
132 base.py 222
133 base.qa 312
134 base.ro 519
135 base.rs 546
136 base.rw 142
137 base.sc 156
138 base.sd 123
151 base.tm 558
152 base.tt 203
153 base.tv 419
154 base.tw 330
155 base.tz 135
156 base.ua 559
157 base.ug 136
158 base.uk 510
159 base.us 225
160 base.uy 223
161 base.uz 560
162 base.vc 234
163 base.ve 201
164 base.vn 325
165 base.vu 415
166 base.za 112
167 base.zm 117
168 base.zw 116 116

View File

@@ -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()

View File

@@ -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):

View File

@@ -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):

View File

@@ -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 = [{}]

View File

@@ -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)

View File

@@ -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:

View File

@@ -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');
}
},
//--------------------------------------------------------------------------

View File

@@ -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);
},
});
});

View File

@@ -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"