Merge branch 'develop' into bank_transaction_currency_symbol_fixes
This commit is contained in:
commit
69a4b8c80b
@ -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))
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
12
erpnext/patches/v13_0/enable_ksa_vat_docs.py
Normal file
12
erpnext/patches/v13_0/enable_ksa_vat_docs.py
Normal 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()
|
@ -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
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
@ -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 = []
|
||||||
|
@ -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
|
||||||
|
@ -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 """
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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, [])
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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 = []
|
||||||
|
Loading…
Reference in New Issue
Block a user