Merge branch 'develop' into bank_transaction_currency_symbol_fixes

This commit is contained in:
Deepesh Garg 2022-03-22 11:52:20 +05:30 committed by GitHub
commit 69a4b8c80b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 432 additions and 197 deletions

View File

@ -32,6 +32,8 @@ class GLEntry(Document):
name will be changed using autoname options (in a scheduled job) name will be changed using autoname options (in a scheduled job)
""" """
self.name = frappe.generate_hash(txt="", length=10) self.name = frappe.generate_hash(txt="", length=10)
if self.meta.autoname == "hash":
self.to_rename = 0
def validate(self): def validate(self):
self.flags.ignore_submit_comment = True self.flags.ignore_submit_comment = True
@ -134,7 +136,7 @@ class GLEntry(Document):
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss": frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and not self.is_cancelled:
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))

View File

@ -4,7 +4,8 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_to_date from frappe.query_builder.functions import Sum
from frappe.utils import add_to_date, get_date_str
from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
@ -28,15 +29,22 @@ def get_mappers_from_db():
def get_accounts_in_mappers(mapping_names): def get_accounts_in_mappers(mapping_names):
return frappe.db.sql(''' cfm = frappe.qb.DocType('Cash Flow Mapping')
select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability, cfma = frappe.qb.DocType('Cash Flow Mapping Accounts')
cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment result = (
from `tabCash Flow Mapping Accounts` cfma frappe.qb
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name .select(
where cfma.parent in (%s) cfma.name, cfm.label, cfm.is_working_capital,
order by cfm.is_working_capital cfm.is_income_tax_liability, cfm.is_income_tax_expense,
''', (', '.join('"%s"' % d for d in mapping_names))) cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account
)
.from_(cfm)
.join(cfma)
.on(cfm.name == cfma.parent)
.where(cfma.parent.isin(mapping_names))
).run()
return result
def setup_mappers(mappers): def setup_mappers(mappers):
cash_flow_accounts = [] cash_flow_accounts = []
@ -57,31 +65,31 @@ def setup_mappers(mappers):
account_types = [ account_types = [
dict( dict(
name=account[0], label=account[1], is_working_capital=account[2], name=account[0], account_name=account[7], label=account[1], is_working_capital=account[2],
is_income_tax_liability=account[3], is_income_tax_expense=account[4] is_income_tax_liability=account[3], is_income_tax_expense=account[4]
) for account in accounts if not account[3]] ) for account in accounts if not account[3]]
finance_costs_adjustments = [ finance_costs_adjustments = [
dict( dict(
name=account[0], label=account[1], is_finance_cost=account[5], name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5],
is_finance_cost_adjustment=account[6] is_finance_cost_adjustment=account[6]
) for account in accounts if account[6]] ) for account in accounts if account[6]]
tax_liabilities = [ tax_liabilities = [
dict( dict(
name=account[0], label=account[1], is_income_tax_liability=account[3], name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
is_income_tax_expense=account[4] is_income_tax_expense=account[4]
) for account in accounts if account[3]] ) for account in accounts if account[3]]
tax_expenses = [ tax_expenses = [
dict( dict(
name=account[0], label=account[1], is_income_tax_liability=account[3], name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
is_income_tax_expense=account[4] is_income_tax_expense=account[4]
) for account in accounts if account[4]] ) for account in accounts if account[4]]
finance_costs = [ finance_costs = [
dict( dict(
name=account[0], label=account[1], is_finance_cost=account[5]) name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5])
for account in accounts if account[5]] for account in accounts if account[5]]
account_types_labels = sorted( account_types_labels = sorted(
@ -124,27 +132,27 @@ def setup_mappers(mappers):
) )
for label in account_types_labels: for label in account_types_labels:
names = [d['name'] for d in account_types if d['label'] == label[0]] names = [d['account_name'] for d in account_types if d['label'] == label[0]]
m = dict(label=label[0], names=names, is_working_capital=label[1]) m = dict(label=label[0], names=names, is_working_capital=label[1])
mapping['account_types'].append(m) mapping['account_types'].append(m)
for label in fc_adjustment_labels: for label in fc_adjustment_labels:
names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]] names = [d['account_name'] for d in finance_costs_adjustments if d['label'] == label[0]]
m = dict(label=label[0], names=names) m = dict(label=label[0], names=names)
mapping['finance_costs_adjustments'].append(m) mapping['finance_costs_adjustments'].append(m)
for label in unique_liability_labels: for label in unique_liability_labels:
names = [d['name'] for d in tax_liabilities if d['label'] == label[0]] names = [d['account_name'] for d in tax_liabilities if d['label'] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping['tax_liabilities'].append(m) mapping['tax_liabilities'].append(m)
for label in unique_expense_labels: for label in unique_expense_labels:
names = [d['name'] for d in tax_expenses if d['label'] == label[0]] names = [d['account_name'] for d in tax_expenses if d['label'] == label[0]]
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
mapping['tax_expenses'].append(m) mapping['tax_expenses'].append(m)
for label in unique_finance_costs_labels: for label in unique_finance_costs_labels:
names = [d['name'] for d in finance_costs if d['label'] == label[0]] names = [d['account_name'] for d in finance_costs if d['label'] == label[0]]
m = dict(label=label[0], names=names, is_finance_cost=label[1]) m = dict(label=label[0], names=names, is_finance_cost=label[1])
mapping['finance_costs'].append(m) mapping['finance_costs'].append(m)
@ -371,14 +379,30 @@ def execute(filters=None):
def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0): def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0):
if not account_names or not account_names[0] or not type(account_names[0]) == str:
# only proceed if account_names is a list of account names
return {}
from erpnext.accounts.report.cash_flow.cash_flow import get_start_date from erpnext.accounts.report.cash_flow.cash_flow import get_start_date
company = filters.company company = filters.company
data = {} data = {}
total = 0 total = 0
GLEntry = frappe.qb.DocType('GL Entry')
Account = frappe.qb.DocType('Account')
for period in period_list: for period in period_list:
start_date = get_start_date(period, accumulated_values, company) start_date = get_start_date(period, accumulated_values, company)
accounts = ', '.join('"%s"' % d for d in account_names)
account_subquery = (
frappe.qb.from_(Account)
.where(
(Account.name.isin(account_names)) |
(Account.parent_account.isin(account_names))
)
.select(Account.name)
.as_("account_subquery")
)
if opening_balances: if opening_balances:
date_info = dict(date=start_date) date_info = dict(date=start_date)
@ -395,32 +419,31 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
else: else:
start, end = add_to_date(**date_info), add_to_date(**date_info) start, end = add_to_date(**date_info), add_to_date(**date_info)
gl_sum = frappe.db.sql_list(""" start, end = get_date_str(start), get_date_str(end)
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
OR parent_account IN (%s))
""", (company, start, end, accounts, accounts))
else:
gl_sum = frappe.db.sql_list("""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
OR parent_account IN (%s))
""", (company, start_date if accumulated_values else period['from_date'],
period['to_date'], accounts, accounts))
if gl_sum and gl_sum[0]:
amount = gl_sum[0]
else: else:
amount = 0 start, end = start_date if accumulated_values else period['from_date'], period['to_date']
start, end = get_date_str(start), get_date_str(end)
total += amount result = (
data.setdefault(period["key"], amount) frappe.qb.from_(GLEntry)
.select(Sum(GLEntry.credit) - Sum(GLEntry.debit))
.where(
(GLEntry.company == company) &
(GLEntry.posting_date >= start) &
(GLEntry.posting_date <= end) &
(GLEntry.voucher_type != 'Period Closing Voucher') &
(GLEntry.account.isin(account_subquery))
)
).run()
if result and result[0]:
gl_sum = result[0][0]
else:
gl_sum = 0
total += gl_sum
data.setdefault(period["key"], gl_sum)
data["total"] = total data["total"] = total
return data return data

View File

@ -199,31 +199,39 @@ def get_mode_of_payment_details(filters):
invoice_list = get_invoices(filters) invoice_list = get_invoices(filters)
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
if invoice_list: if invoice_list:
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date, inv_mop_detail = frappe.db.sql("""
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount select t.owner,
from `tabSales Invoice` a, `tabSales Invoice Payment` b t.posting_date,
where a.name = b.parent t.mode_of_payment,
and a.docstatus = 1 sum(t.paid_amount) as paid_amount
and a.name in ({invoice_list_names}) from (
group by a.owner, a.posting_date, mode_of_payment select a.owner, a.posting_date,
union ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
select a.owner,a.posting_date, from `tabSales Invoice` a, `tabSales Invoice Payment` b
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_paid_amount) as paid_amount where a.name = b.parent
from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c and a.docstatus = 1
where a.name = c.reference_name and a.name in ({invoice_list_names})
and b.name = c.parent group by a.owner, a.posting_date, mode_of_payment
and b.docstatus = 1 union
and a.name in ({invoice_list_names}) select a.owner,a.posting_date,
group by a.owner, a.posting_date, mode_of_payment ifnull(b.mode_of_payment, '') as mode_of_payment, sum(c.allocated_amount) as paid_amount
union from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
select a.owner, a.posting_date, where a.name = c.reference_name
ifnull(a.voucher_type,'') as mode_of_payment, sum(b.credit) and b.name = c.parent
from `tabJournal Entry` a, `tabJournal Entry Account` b and b.docstatus = 1
where a.name = b.parent and a.name in ({invoice_list_names})
and a.docstatus = 1 group by a.owner, a.posting_date, mode_of_payment
and b.reference_type = "Sales Invoice" union
and b.reference_name in ({invoice_list_names}) select a.owner, a.posting_date,
group by a.owner, a.posting_date, mode_of_payment ifnull(a.voucher_type,'') as mode_of_payment, sum(b.credit)
from `tabJournal Entry` a, `tabJournal Entry Account` b
where a.name = b.parent
and a.docstatus = 1
and b.reference_type = "Sales Invoice"
and b.reference_name in ({invoice_list_names})
group by a.owner, a.posting_date, mode_of_payment
) t
group by t.owner, t.posting_date, t.mode_of_payment
""".format(invoice_list_names=invoice_list_names), as_dict=1) """.format(invoice_list_names=invoice_list_names), as_dict=1)
inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date, inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date,
@ -231,7 +239,7 @@ def get_mode_of_payment_details(filters):
from `tabSales Invoice` a, `tabSales Invoice Payment` b from `tabSales Invoice` a, `tabSales Invoice Payment` b
where a.name = b.parent where a.name = b.parent
and a.name in ({invoice_list_names}) and a.name in ({invoice_list_names})
and b.mode_of_payment = 'Cash' and b.type = 'Cash'
and a.base_change_amount > 0 and a.base_change_amount > 0
group by a.owner, a.posting_date, mode_of_payment""".format(invoice_list_names=invoice_list_names), as_dict=1) group by a.owner, a.posting_date, mode_of_payment""".format(invoice_list_names=invoice_list_names), as_dict=1)

View File

@ -8,13 +8,14 @@ from math import ceil
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate
class LeavePolicyAssignment(Document): class LeavePolicyAssignment(Document):
def validate(self): def validate(self):
self.validate_policy_assignment_overlap()
self.set_dates() self.set_dates()
self.validate_policy_assignment_overlap()
self.warn_about_carry_forwarding()
def on_submit(self): def on_submit(self):
self.grant_leave_alloc_for_employee() self.grant_leave_alloc_for_employee()
@ -38,6 +39,20 @@ class LeavePolicyAssignment(Document):
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
def warn_about_carry_forwarding(self):
if not self.carry_forward:
return
leave_types = get_leave_type_details()
leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
for policy in leave_policy.leave_policy_details:
leave_type = leave_types.get(policy.leave_type)
if not leave_type.is_carry_forward:
msg = _("Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled.").format(
frappe.bold(get_link_to_form("Leave Type", leave_type.name)))
frappe.msgprint(msg, indicator="orange", alert=True)
@frappe.whitelist() @frappe.whitelist()
def grant_leave_alloc_for_employee(self): def grant_leave_alloc_for_employee(self):
if self.leaves_allocated: if self.leaves_allocated:

View File

@ -1018,21 +1018,21 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
required_qty = item.get("quantity") required_qty = item.get("quantity")
# get available material by transferring to production warehouse # get available material by transferring to production warehouse
for d in locations: for d in locations:
if required_qty <=0: return if required_qty <= 0:
return
new_dict = copy.deepcopy(item) new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty") quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
if required_qty > 0: new_dict.update({
new_dict.update({ "quantity": quantity,
"quantity": quantity, "material_request_type": "Material Transfer",
"material_request_type": "Material Transfer", "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM "from_warehouse": d.get("warehouse")
"from_warehouse": d.get("warehouse") })
})
required_qty -= quantity required_qty -= quantity
new_mr_items.append(new_dict) new_mr_items.append(new_dict)
# raise purchase request for remaining qty # raise purchase request for remaining qty
if required_qty: if required_qty:

View File

@ -333,6 +333,7 @@ erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning erpnext.patches.v13_0.non_profit_deprecation_warning
erpnext.patches.v13_0.enable_ksa_vat_docs #1
[post_model_sync] [post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents

View File

@ -0,0 +1,12 @@
import frappe
from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if not company:
return
add_print_formats()
add_permissions()

View File

@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
fieldtype:'Float', fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no, read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'), label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
default: flt(me.item.stock_qty), default: flt(me.item.stock_qty) || flt(me.item.transfer_qty),
}, },
...get_pending_qty_fields(me), ...get_pending_qty_fields(me),
{ {
@ -94,14 +94,16 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
description: __('Fetch Serial Numbers based on FIFO'), description: __('Fetch Serial Numbers based on FIFO'),
click: () => { click: () => {
let qty = this.dialog.fields_dict.qty.get_value(); let qty = this.dialog.fields_dict.qty.get_value();
let already_selected_serial_nos = get_selected_serial_nos(me);
let numbers = frappe.call({ let numbers = frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
args: { args: {
qty: qty, qty: qty,
item_code: me.item_code, item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
batch_no: me.item.batch_no || null, batch_nos: me.item.batch_no || null,
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
exclude_sr_nos: already_selected_serial_nos
} }
}); });
@ -577,15 +579,29 @@ function get_pending_qty_fields(me) {
return pending_qty_fields; return pending_qty_fields;
} }
function calc_total_selected_qty(me) { // get all items with same item code except row for which selector is open.
function get_rows_with_same_item_code(me) {
const { frm: { doc: { items }}, item: { name, item_code }} = me; const { frm: { doc: { items }}, item: { name, item_code }} = me;
const totalSelectedQty = items return items.filter(item => (item.name !== name) && (item.item_code === item_code))
.filter( item => ( item.name !== name ) && ( item.item_code === item_code ) ) }
.map( item => flt(item.qty) )
.reduce( (i, j) => i + j, 0); function calc_total_selected_qty(me) {
const totalSelectedQty = get_rows_with_same_item_code(me)
.map(item => flt(item.qty))
.reduce((i, j) => i + j, 0);
return totalSelectedQty; return totalSelectedQty;
} }
function get_selected_serial_nos(me) {
const selected_serial_nos = get_rows_with_same_item_code(me)
.map(item => item.serial_no)
.filter(serial => serial)
.map(sr_no_string => sr_no_string.split('\n'))
.reduce((acc, arr) => acc.concat(arr), [])
.filter(serial => serial);
return selected_serial_nos;
};
function check_can_calculate_pending_qty(me) { function check_can_calculate_pending_qty(me) {
const { frm: { doc }, item } = me; const { frm: { doc }, item } = me;
const docChecks = doc.bom_no const docChecks = doc.bom_no

View File

@ -82,62 +82,42 @@ frappe.query_reports["Sales Analytics"] = {
const tree_type = frappe.query_report.filters[0].value; const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return; if (data_doctype != tree_type) return;
row_name = data[2].content; const row_name = data[2].content;
length = data.length; const raw_data = frappe.query_report.chart.data;
const new_datasets = raw_data.datasets;
if (tree_type == "Customer") { const element_found = new_datasets.some(
row_values = data (element, index, array) => {
.slice(4, length - 1) if (element.name == row_name) {
.map(function (column) { array.splice(index, 1);
return column.content; return true;
}); }
} else if (tree_type == "Item") { return false;
row_values = data
.slice(5, length - 1)
.map(function (column) {
return column.content;
});
} else {
row_values = data
.slice(3, length - 1)
.map(function (column) {
return column.content;
});
}
entry = {
name: row_name,
values: row_values,
};
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
array.splice(index, 1)
return true
} }
return false );
}) const slice_at = { Customer: 4, Item: 5 }[tree_type] || 3;
if (!element_found) { if (!element_found) {
new_datasets.push(entry); new_datasets.push({
name: row_name,
values: data
.slice(slice_at, data.length - 1)
.map(column => column.content),
});
} }
let new_data = { const new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets, datasets: new_datasets,
}; };
chart_options = {
frappe.query_report.render_chart({
data: new_data, data: new_data,
type: "line", type: "line",
}; });
frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
}, },
}); });
}, },
} };

View File

@ -107,6 +107,7 @@ class Item(Document):
self.validate_variant_attributes() self.validate_variant_attributes()
self.validate_variant_based_on_change() self.validate_variant_based_on_change()
self.validate_fixed_asset() self.validate_fixed_asset()
self.clear_retain_sample()
self.validate_retain_sample() self.validate_retain_sample()
self.validate_uom_conversion_factor() self.validate_uom_conversion_factor()
self.validate_customer_provided_part() self.validate_customer_provided_part()
@ -209,6 +210,13 @@ class Item(Document):
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
self.item_code)) self.item_code))
def clear_retain_sample(self):
if not self.has_batch_no:
self.retain_sample = None
if not self.retain_sample:
self.sample_quantity = None
def add_default_uom_in_conversion_factor_table(self): def add_default_uom_in_conversion_factor_table(self):
if not self.is_new() and self.has_value_changed("stock_uom"): if not self.is_new() and self.has_value_changed("stock_uom"):
self.uoms = [] self.uoms = []

View File

@ -656,6 +656,19 @@ class TestItem(FrappeTestCase):
make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5)) make_stock_entry(qty=1, item_code=item.name, target="_Test Warehouse - _TC", posting_date = add_days(today(), 5))
self.consume_item_code_with_differet_stock_transactions(item_code=item.name) self.consume_item_code_with_differet_stock_transactions(item_code=item.name)
@change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"})
def test_retain_sample(self):
item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1})
self.assertEqual(item.has_batch_no, 1)
self.assertEqual(item.retain_sample, 1)
self.assertEqual(item.sample_quantity, 1)
item.has_batch_no = None
item.save()
self.assertEqual(item.retain_sample, None)
self.assertEqual(item.sample_quantity, None)
item.delete()
def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"): def consume_item_code_with_differet_stock_transactions(self, item_code, warehouse="_Test Warehouse - _TC"):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice

View File

@ -82,6 +82,9 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def before_update_after_submit(self):
self.validate_schedule_date()
def validate_material_request_type(self): def validate_material_request_type(self):
""" Validate fields in accordance with selected type """ """ Validate fields in accordance with selected type """

View File

@ -177,6 +177,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"allow_on_submit": 1,
"bold": 1, "bold": 1,
"columns": 2, "columns": 2,
"fieldname": "schedule_date", "fieldname": "schedule_date",
@ -459,7 +460,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-11-03 14:40:24.409826", "modified": "2022-03-10 18:42:42.705190",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Material Request Item", "name": "Material Request Item",

View File

@ -7,7 +7,17 @@ import json
import frappe import frappe
from frappe import ValidationError, _ from frappe import ValidationError, _
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate from frappe.query_builder.functions import Coalesce
from frappe.utils import (
add_days,
cint,
cstr,
flt,
get_link_to_form,
getdate,
nowdate,
safe_json_loads,
)
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.stock.get_item_details import get_reserved_qty_for_so from erpnext.stock.get_item_details import get_reserved_qty_for_so
@ -564,26 +574,33 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
return serial_nos return serial_nos
@frappe.whitelist() @frappe.whitelist()
def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None): def auto_fetch_serial_number(qty, item_code, warehouse,
filters = { "item_code": item_code, "warehouse": warehouse } posting_date=None, batch_nos=None, for_doctype=None, exclude_sr_nos=None):
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
if exclude_sr_nos is None:
exclude_sr_nos = []
else:
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
if batch_nos: if batch_nos:
try: batch_nos = safe_json_loads(batch_nos)
filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)] if isinstance(batch_nos, list):
except Exception: filters.batch_no = batch_nos
filters["batch_no"] = [batch_nos] elif isinstance(batch_nos, str):
filters.batch_no = [batch_nos]
if posting_date: if posting_date:
filters["expiry_date"] = posting_date filters.expiry_date = posting_date
serial_numbers = [] serial_numbers = []
if for_doctype == 'POS Invoice': if for_doctype == 'POS Invoice':
reserved_sr_nos = get_pos_reserved_serial_nos(filters) exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters))
serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos)
else:
serial_numbers = fetch_serial_numbers(filters, qty)
return [d.get('name') for d in serial_numbers] serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos)
return sorted([d.get('name') for d in serial_numbers])
@frappe.whitelist() @frappe.whitelist()
def get_pos_reserved_serial_nos(filters): def get_pos_reserved_serial_nos(filters):
@ -610,37 +627,37 @@ def get_pos_reserved_serial_nos(filters):
def fetch_serial_numbers(filters, qty, do_not_include=None): def fetch_serial_numbers(filters, qty, do_not_include=None):
if do_not_include is None: if do_not_include is None:
do_not_include = [] do_not_include = []
batch_join_selection = ""
batch_no_condition = ""
batch_nos = filters.get("batch_no") batch_nos = filters.get("batch_no")
expiry_date = filters.get("expiry_date") expiry_date = filters.get("expiry_date")
serial_no = frappe.qb.DocType("Serial No")
query = (
frappe.qb
.from_(serial_no)
.select(serial_no.name)
.where(
(serial_no.item_code == filters["item_code"])
& (serial_no.warehouse == filters["warehouse"])
& (Coalesce(serial_no.sales_invoice, "") == "")
& (Coalesce(serial_no.delivery_document_no, "") == "")
)
.orderby(serial_no.creation)
.limit(qty or 1)
)
if do_not_include:
query = query.where(serial_no.name.notin(do_not_include))
if batch_nos: if batch_nos:
batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos)) query = query.where(serial_no.batch_no.isin(batch_nos))
if expiry_date: if expiry_date:
batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name " batch = frappe.qb.DocType("Batch")
expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s " query = (query
batch_no_condition += expiry_date_cond .left_join(batch).on(serial_no.batch_no == batch.name)
.where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date)
excluded_sr_nos = ", ".join(["" + frappe.db.escape(sr) + "" for sr in do_not_include]) or "''" )
serial_numbers = frappe.db.sql("""
SELECT sr.name FROM `tabSerial No` sr {batch_join_selection}
WHERE
sr.name not in ({excluded_sr_nos}) AND
sr.item_code = %(item_code)s AND
sr.warehouse = %(warehouse)s AND
ifnull(sr.sales_invoice,'') = '' AND
ifnull(sr.delivery_document_no, '') = ''
{batch_no_condition}
ORDER BY
sr.creation
LIMIT
{qty}
""".format(
excluded_sr_nos=excluded_sr_nos,
qty=qty or 1,
batch_join_selection=batch_join_selection,
batch_no_condition=batch_no_condition
), filters, as_dict=1)
serial_numbers = query.run(as_dict=True)
return serial_numbers return serial_numbers

View File

@ -6,10 +6,12 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@ -18,9 +20,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
test_dependencies = ["Item"] test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No') test_records = frappe.get_test_records('Serial No')
from frappe.tests.utils import FrappeTestCase
from erpnext.stock.doctype.serial_no.serial_no import *
class TestSerialNo(FrappeTestCase): class TestSerialNo(FrappeTestCase):
@ -242,3 +241,56 @@ class TestSerialNo(FrappeTestCase):
) )
self.assertEqual(value_diff, -113) self.assertEqual(value_diff, -113)
def test_auto_fetch(self):
item_code = make_item(properties={
"has_serial_no": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"serial_no_series": "TEST.#######"
}).name
warehouse = "_Test Warehouse - _TC"
in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5)
in1.reload()
in2.reload()
batch1 = in1.items[0].batch_no
batch2 = in2.items[0].batch_no
batch_wise_serials = {
batch1 : get_serial_nos(in1.items[0].serial_no),
batch2: get_serial_nos(in2.items[0].serial_no)
}
# Test FIFO
first_fetch = auto_fetch_serial_number(5, item_code, warehouse)
self.assertEqual(first_fetch, batch_wise_serials[batch1])
# partial FIFO
partial_fetch = auto_fetch_serial_number(2, item_code, warehouse)
self.assertTrue(set(partial_fetch).issubset(set(first_fetch)),
msg=f"{partial_fetch} should be subset of {first_fetch}")
# exclusion
remaining = auto_fetch_serial_number(3, item_code, warehouse, exclude_sr_nos=partial_fetch)
self.assertEqual(sorted(remaining + partial_fetch), first_fetch)
# batchwise
for batch, expected_serials in batch_wise_serials.items():
fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch)
self.assertEqual(fetched_sr, sorted(expected_serials))
# non existing warehouse
self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), [])
# multi batch
all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list]
fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()))
self.assertEqual(sorted(all_serials), fetched_serials)
# expiry date
frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01")
non_expired_serials = auto_fetch_serial_number(5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1)
self.assertEqual(non_expired_serials, [])

View File

@ -26,6 +26,8 @@ class StockLedgerEntry(Document):
name will be changed using autoname options (in a scheduled job) name will be changed using autoname options (in a scheduled job)
""" """
self.name = frappe.generate_hash(txt="", length=10) self.name = frappe.generate_hash(txt="", length=10)
if self.meta.autoname == "hash":
self.to_rename = 0
def validate(self): def validate(self):
self.flags.ignore_submit_comment = True self.flags.ignore_submit_comment = True

View File

@ -7,9 +7,11 @@ from uuid import uuid4
import frappe import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today from frappe.utils import add_days, today
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@ -939,3 +941,62 @@ def get_unique_suffix():
# Used to isolate valuation sensitive # Used to isolate valuation sensitive
# tests to prevent future tests from failing. # tests to prevent future tests from failing.
return str(uuid4())[:8].upper() return str(uuid4())[:8].upper()
class TestDeferredNaming(FrappeTestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.gle_autoname = frappe.get_meta("GL Entry").autoname
cls.sle_autoname = frappe.get_meta("Stock Ledger Entry").autoname
def setUp(self) -> None:
self.item = make_item().name
self.warehouse = "Stores - TCP1"
self.company = "_Test Company with perpetual inventory"
def tearDown(self) -> None:
make_property_setter(doctype="GL Entry", for_doctype=True,
property="autoname", value=self.gle_autoname, property_type="Data", fieldname=None)
make_property_setter(doctype="Stock Ledger Entry", for_doctype=True,
property="autoname", value=self.sle_autoname, property_type="Data", fieldname=None)
# since deferred naming autocommits, commit all changes to avoid flake
frappe.db.commit() # nosemgrep
@staticmethod
def get_gle_sles(se):
filters = {"voucher_type": se.doctype, "voucher_no": se.name}
gle = set(frappe.get_list("GL Entry", filters, pluck="name"))
sle = set(frappe.get_list("Stock Ledger Entry", filters, pluck="name"))
return gle, sle
def test_deferred_naming(self):
se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse,
qty=10, rate=100, company=self.company)
gle, sle = self.get_gle_sles(se)
rename_gle_sle_docs()
renamed_gle, renamed_sle = self.get_gle_sles(se)
self.assertFalse(gle & renamed_gle, msg="GLEs not renamed")
self.assertFalse(sle & renamed_sle, msg="SLEs not renamed")
se.cancel()
def test_hash_naming(self):
# disable naming series
for doctype in ("GL Entry", "Stock Ledger Entry"):
make_property_setter(doctype=doctype, for_doctype=True,
property="autoname", value="hash", property_type="Data", fieldname=None)
se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse,
qty=10, rate=100, company=self.company)
gle, sle = self.get_gle_sles(se)
rename_gle_sle_docs()
renamed_gle, renamed_sle = self.get_gle_sles(se)
self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming")
self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming")
se.cancel()

View File

@ -4,12 +4,12 @@
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import cint
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children
test_records = frappe.get_test_records('Warehouse') test_records = frappe.get_test_records('Warehouse')
@ -65,6 +65,33 @@ class TestWarehouse(FrappeTestCase):
f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}."
) )
def test_group_non_group_conversion(self):
warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion"))
convert_to_group_or_ledger(warehouse.name)
warehouse.reload()
self.assertEqual(warehouse.is_group, 1)
child = create_warehouse("GroupWHChild", {"parent_warehouse": warehouse.name})
# chid exists
self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
frappe.delete_doc("Warehouse", child)
convert_to_group_or_ledger(warehouse.name)
warehouse.reload()
self.assertEqual(warehouse.is_group, 0)
make_stock_entry(item_code="_Test Item", target=warehouse.name, qty=1)
# SLE exists
self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
def test_get_children(self):
company = "_Test Company"
children = get_children("Warehouse", parent=company, company=company, is_root=True)
self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children))
def create_warehouse(warehouse_name, properties=None, company=None): def create_warehouse(warehouse_name, properties=None, company=None):
if not company: if not company:

View File

@ -41,14 +41,11 @@ class Warehouse(NestedSet):
def on_trash(self): def on_trash(self):
# delete bin # delete bin
bins = frappe.db.sql("select * from `tabBin` where warehouse = %s", bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name})
self.name, as_dict=1)
for d in bins: for d in bins:
if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \ if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \
d['indented_qty'] or d['projected_qty'] or d['planned_qty']: d['indented_qty'] or d['projected_qty'] or d['planned_qty']:
throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code'])) throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code']))
else:
frappe.db.sql("delete from `tabBin` where name = %s", d['name'])
if self.check_if_sle_exists(): if self.check_if_sle_exists():
throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse.")) throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse."))
@ -56,16 +53,15 @@ class Warehouse(NestedSet):
if self.check_if_child_exists(): if self.check_if_child_exists():
throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse.")) throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
frappe.db.delete("Bin", filters={"warehouse": self.name})
self.update_nsm_model() self.update_nsm_model()
self.unlink_from_items() self.unlink_from_items()
def check_if_sle_exists(self): def check_if_sle_exists(self):
return frappe.db.sql("""select name from `tabStock Ledger Entry` return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
where warehouse = %s limit 1""", self.name)
def check_if_child_exists(self): def check_if_child_exists(self):
return frappe.db.sql("""select name from `tabWarehouse` return frappe.db.exists("Warehouse", {"parent_warehouse": self.name})
where parent_warehouse = %s limit 1""", self.name)
def convert_to_group_or_ledger(self): def convert_to_group_or_ledger(self):
if self.is_group: if self.is_group:
@ -92,10 +88,7 @@ class Warehouse(NestedSet):
return 1 return 1
def unlink_from_items(self): def unlink_from_items(self):
frappe.db.sql(""" frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None)
update `tabItem Default`
set default_warehouse=NULL
where default_warehouse=%s""", self.name)
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False): def get_children(doctype, parent=None, company=None, is_root=False):
@ -164,15 +157,16 @@ def add_node():
frappe.get_doc(args).insert() frappe.get_doc(args).insert()
@frappe.whitelist() @frappe.whitelist()
def convert_to_group_or_ledger(): def convert_to_group_or_ledger(docname=None):
args = frappe.form_dict if not docname:
return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger() docname = frappe.form_dict.docname
return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger()
def get_child_warehouses(warehouse): def get_child_warehouses(warehouse):
lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"]) from frappe.utils.nestedset import get_descendants_of
return frappe.db.sql_list("""select name from `tabWarehouse` children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
where lft >= %s and rgt <= %s""", (lft, rgt)) return children + [warehouse] # append self for backward compatibility
def get_warehouses_based_on_account(account, company=None): def get_warehouses_based_on_account(account, company=None):
warehouses = [] warehouses = []