Merge branch 'develop' into bank_transaction_currency_symbol_fixes
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 9d1452b..6f7a0b2 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -32,6 +32,8 @@
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
@@ -134,7 +136,7 @@
def check_pl_account(self):
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")
.format(self.voucher_type, self.voucher_no, self.account))
diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
index 45d147e..20f7fcf 100644
--- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
@@ -4,7 +4,8 @@
import frappe
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.profit_and_loss_statement.profit_and_loss_statement import (
@@ -28,15 +29,22 @@
def get_accounts_in_mappers(mapping_names):
- return frappe.db.sql('''
- select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability,
- cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment
- from `tabCash Flow Mapping Accounts` cfma
- join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
- where cfma.parent in (%s)
- order by cfm.is_working_capital
- ''', (', '.join('"%s"' % d for d in mapping_names)))
+ cfm = frappe.qb.DocType('Cash Flow Mapping')
+ cfma = frappe.qb.DocType('Cash Flow Mapping Accounts')
+ result = (
+ frappe.qb
+ .select(
+ cfma.name, cfm.label, cfm.is_working_capital,
+ cfm.is_income_tax_liability, cfm.is_income_tax_expense,
+ 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):
cash_flow_accounts = []
@@ -57,31 +65,31 @@
account_types = [
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]
) for account in accounts if not account[3]]
finance_costs_adjustments = [
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]
) for account in accounts if account[6]]
tax_liabilities = [
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]
) for account in accounts if account[3]]
tax_expenses = [
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]
) for account in accounts if account[4]]
finance_costs = [
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]]
account_types_labels = sorted(
@@ -124,27 +132,27 @@
)
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])
mapping['account_types'].append(m)
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)
mapping['finance_costs_adjustments'].append(m)
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])
mapping['tax_liabilities'].append(m)
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])
mapping['tax_expenses'].append(m)
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])
mapping['finance_costs'].append(m)
@@ -371,14 +379,30 @@
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
company = filters.company
data = {}
total = 0
+ GLEntry = frappe.qb.DocType('GL Entry')
+ Account = frappe.qb.DocType('Account')
+
for period in period_list:
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:
date_info = dict(date=start_date)
@@ -395,32 +419,31 @@
else:
start, end = add_to_date(**date_info), add_to_date(**date_info)
- 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, 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))
+ start, end = get_date_str(start), get_date_str(end)
- if gl_sum and gl_sum[0]:
- amount = gl_sum[0]
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
- data.setdefault(period["key"], amount)
+ result = (
+ 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
return data
diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
index 3b73628..904513c 100644
--- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
+++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
@@ -199,31 +199,39 @@
invoice_list = get_invoices(filters)
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
if invoice_list:
- inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
- ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
- from `tabSales Invoice` a, `tabSales Invoice Payment` b
- where a.name = b.parent
- and a.docstatus = 1
- and a.name in ({invoice_list_names})
- group by a.owner, a.posting_date, mode_of_payment
- union
- select a.owner,a.posting_date,
- ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_paid_amount) as paid_amount
- from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
- where a.name = c.reference_name
- and b.name = c.parent
- and b.docstatus = 1
- and a.name in ({invoice_list_names})
- group by a.owner, a.posting_date, mode_of_payment
- union
- select a.owner, a.posting_date,
- 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
+ inv_mop_detail = frappe.db.sql("""
+ select t.owner,
+ t.posting_date,
+ t.mode_of_payment,
+ sum(t.paid_amount) as paid_amount
+ from (
+ select a.owner, a.posting_date,
+ ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
+ from `tabSales Invoice` a, `tabSales Invoice Payment` b
+ where a.name = b.parent
+ and a.docstatus = 1
+ and a.name in ({invoice_list_names})
+ group by a.owner, a.posting_date, mode_of_payment
+ union
+ select a.owner,a.posting_date,
+ ifnull(b.mode_of_payment, '') as mode_of_payment, sum(c.allocated_amount) as paid_amount
+ from `tabSales Invoice` a, `tabPayment Entry` b,`tabPayment Entry Reference` c
+ where a.name = c.reference_name
+ and b.name = c.parent
+ and b.docstatus = 1
+ and a.name in ({invoice_list_names})
+ group by a.owner, a.posting_date, mode_of_payment
+ union
+ select a.owner, a.posting_date,
+ 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)
inv_change_amount = frappe.db.sql("""select a.owner, a.posting_date,
@@ -231,7 +239,7 @@
from `tabSales Invoice` a, `tabSales Invoice Payment` b
where a.name = b.parent
and a.name in ({invoice_list_names})
- and b.mode_of_payment = 'Cash'
+ and b.type = 'Cash'
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)
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index c11a821..ae5ac7b 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -8,13 +8,14 @@
import frappe
from frappe import _, bold
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):
def validate(self):
- self.validate_policy_assignment_overlap()
self.set_dates()
+ self.validate_policy_assignment_overlap()
+ self.warn_about_carry_forwarding()
def on_submit(self):
self.grant_leave_alloc_for_employee()
@@ -38,6 +39,20 @@
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))))
+ 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()
def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 48cd753..2b6e696 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1018,21 +1018,21 @@
required_qty = item.get("quantity")
# get available material by transferring to production warehouse
for d in locations:
- if required_qty <=0: return
+ if required_qty <= 0:
+ return
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
- if required_qty > 0:
- new_dict.update({
- "quantity": quantity,
- "material_request_type": "Material Transfer",
- "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
- "from_warehouse": d.get("warehouse")
- })
+ new_dict.update({
+ "quantity": quantity,
+ "material_request_type": "Material Transfer",
+ "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
+ "from_warehouse": d.get("warehouse")
+ })
- required_qty -= quantity
- new_mr_items.append(new_dict)
+ required_qty -= quantity
+ new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty:
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 16d8c73..3199912 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,6 +333,7 @@
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
+erpnext.patches.v13_0.enable_ksa_vat_docs #1
[post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py
new file mode 100644
index 0000000..3f48262
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_ksa_vat_docs.py
@@ -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()
\ No newline at end of file
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 08270bd..81ff351 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -75,7 +75,7 @@
fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no,
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),
{
@@ -94,14 +94,16 @@
description: __('Fetch Serial Numbers based on FIFO'),
click: () => {
let qty = this.dialog.fields_dict.qty.get_value();
+ let already_selected_serial_nos = get_selected_serial_nos(me);
let numbers = frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
args: {
qty: qty,
item_code: me.item_code,
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
- batch_no: me.item.batch_no || null,
- posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
+ batch_nos: me.item.batch_no || null,
+ posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
+ exclude_sr_nos: already_selected_serial_nos
}
});
@@ -577,15 +579,29 @@
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 totalSelectedQty = items
- .filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
- .map( item => flt(item.qty) )
- .reduce( (i, j) => i + j, 0);
+ return items.filter(item => (item.name !== name) && (item.item_code === item_code))
+}
+
+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;
}
+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) {
const { frm: { doc }, item } = me;
const docChecks = doc.bom_no
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
index 6b03c7d..d527e42 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -82,62 +82,42 @@
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
- row_name = data[2].content;
- length = data.length;
-
- if (tree_type == "Customer") {
- row_values = data
- .slice(4, length - 1)
- .map(function (column) {
- return column.content;
- });
- } else if (tree_type == "Item") {
- 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
+ const row_name = data[2].content;
+ const raw_data = frappe.query_report.chart.data;
+ const new_datasets = raw_data.datasets;
+ const element_found = new_datasets.some(
+ (element, index, array) => {
+ if (element.name == row_name) {
+ array.splice(index, 1);
+ return true;
+ }
+ return false;
}
- return false
- })
+ );
+ const slice_at = { Customer: 4, Item: 5 }[tree_type] || 3;
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,
datasets: new_datasets,
};
- chart_options = {
+
+ frappe.query_report.render_chart({
data: new_data,
type: "line",
- };
- frappe.query_report.render_chart(chart_options);
+ });
frappe.query_report.raw_chart_data = new_data;
},
},
});
},
-}
+};
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 8ede955..3abeecd 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -107,6 +107,7 @@
self.validate_variant_attributes()
self.validate_variant_based_on_change()
self.validate_fixed_asset()
+ self.clear_retain_sample()
self.validate_retain_sample()
self.validate_uom_conversion_factor()
self.validate_customer_provided_part()
@@ -209,6 +210,13 @@
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
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):
if not self.is_new() and self.has_value_changed("stock_uom"):
self.uoms = []
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 669cabc..112420f 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -656,6 +656,19 @@
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)
+ @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"):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 51209ac..49fefae 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -82,6 +82,9 @@
self.reset_default_field_value("set_warehouse", "items", "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):
""" Validate fields in accordance with selected type """
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json
index 2bad42a..dd66cff 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.json
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.json
@@ -177,6 +177,7 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"bold": 1,
"columns": 2,
"fieldname": "schedule_date",
@@ -459,7 +460,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-11-03 14:40:24.409826",
+ "modified": "2022-03-10 18:42:42.705190",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request Item",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index bf62f50..c77c6c3 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -7,7 +7,17 @@
import frappe
from frappe import ValidationError, _
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.stock.get_item_details import get_reserved_qty_for_so
@@ -564,26 +574,33 @@
return serial_nos
@frappe.whitelist()
-def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None):
- filters = { "item_code": item_code, "warehouse": warehouse }
+def auto_fetch_serial_number(qty, item_code, 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:
- try:
- filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
- except Exception:
- filters["batch_no"] = [batch_nos]
+ batch_nos = safe_json_loads(batch_nos)
+ if isinstance(batch_nos, list):
+ filters.batch_no = batch_nos
+ elif isinstance(batch_nos, str):
+ filters.batch_no = [batch_nos]
if posting_date:
- filters["expiry_date"] = posting_date
+ filters.expiry_date = posting_date
serial_numbers = []
if for_doctype == 'POS Invoice':
- reserved_sr_nos = 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)
+ exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters))
- 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()
def get_pos_reserved_serial_nos(filters):
@@ -610,37 +627,37 @@
def fetch_serial_numbers(filters, qty, do_not_include=None):
if do_not_include is None:
do_not_include = []
- batch_join_selection = ""
- batch_no_condition = ""
+
batch_nos = filters.get("batch_no")
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:
- 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:
- batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name "
- expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s "
- batch_no_condition += expiry_date_cond
+ batch = frappe.qb.DocType("Batch")
+ query = (query
+ .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
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 057a7d4..cca6307 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -6,10 +6,12 @@
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.item.test_item import make_item
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.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -18,9 +20,6 @@
test_dependencies = ["Item"]
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):
@@ -242,3 +241,56 @@
)
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, [])
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index c538307..2f59304 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -26,6 +26,8 @@
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 2352235..fc57995 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -7,9 +7,11 @@
import frappe
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.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.item.test_item import make_item
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@@ -939,3 +941,62 @@
# Used to isolate valuation sensitive
# tests to prevent future tests from failing.
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()
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index cdb7719..08d7c99 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -4,12 +4,12 @@
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import cint
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.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')
@@ -65,6 +65,33 @@
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):
if not company:
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 9cfad86..4c7f41d 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -41,14 +41,11 @@
def on_trash(self):
# delete bin
- bins = frappe.db.sql("select * from `tabBin` where warehouse = %s",
- self.name, as_dict=1)
+ bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name})
for d in bins:
if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \
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']))
- else:
- frappe.db.sql("delete from `tabBin` where name = %s", d['name'])
if self.check_if_sle_exists():
throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse."))
@@ -56,16 +53,15 @@
if self.check_if_child_exists():
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.unlink_from_items()
def check_if_sle_exists(self):
- return frappe.db.sql("""select name from `tabStock Ledger Entry`
- where warehouse = %s limit 1""", self.name)
+ return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
def check_if_child_exists(self):
- return frappe.db.sql("""select name from `tabWarehouse`
- where parent_warehouse = %s limit 1""", self.name)
+ return frappe.db.exists("Warehouse", {"parent_warehouse": self.name})
def convert_to_group_or_ledger(self):
if self.is_group:
@@ -92,10 +88,7 @@
return 1
def unlink_from_items(self):
- frappe.db.sql("""
- update `tabItem Default`
- set default_warehouse=NULL
- where default_warehouse=%s""", self.name)
+ frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None)
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False):
@@ -164,15 +157,16 @@
frappe.get_doc(args).insert()
@frappe.whitelist()
-def convert_to_group_or_ledger():
- args = frappe.form_dict
- return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger()
+def convert_to_group_or_ledger(docname=None):
+ if not docname:
+ docname = frappe.form_dict.docname
+ return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger()
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`
- where lft >= %s and rgt <= %s""", (lft, rgt))
+ children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
+ return children + [warehouse] # append self for backward compatibility
def get_warehouses_based_on_account(account, company=None):
warehouses = []