Merge branch 'develop' into fix-pos-issues-again
This commit is contained in:
commit
4aabe97565
@ -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))
|
||||||
|
|
||||||
|
@ -110,13 +110,12 @@
|
|||||||
"description": "Reference number of the invoice from the previous system",
|
"description": "Reference number of the invoice from the previous system",
|
||||||
"fieldname": "invoice_number",
|
"fieldname": "invoice_number",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Invoice Number"
|
"label": "Invoice Number"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-12-17 19:25:06.053187",
|
"modified": "2022-03-21 19:31:45.382656",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool Item",
|
"name": "Opening Invoice Creation Tool Item",
|
||||||
@ -125,5 +124,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -488,16 +488,15 @@ class POSInvoice(SalesInvoice):
|
|||||||
"payment_account": pay.account,
|
"payment_account": pay.account,
|
||||||
}, ["name"])
|
}, ["name"])
|
||||||
|
|
||||||
args = {
|
filters = {
|
||||||
'doctype': 'Payment Request',
|
|
||||||
'reference_doctype': 'POS Invoice',
|
'reference_doctype': 'POS Invoice',
|
||||||
'reference_name': self.name,
|
'reference_name': self.name,
|
||||||
'payment_gateway_account': payment_gateway_account,
|
'payment_gateway_account': payment_gateway_account,
|
||||||
'email_to': self.contact_mobile
|
'email_to': self.contact_mobile
|
||||||
}
|
}
|
||||||
pr = frappe.db.exists(args)
|
pr = frappe.db.get_value('Payment Request', filters=filters)
|
||||||
if pr:
|
if pr:
|
||||||
return frappe.get_doc('Payment Request', pr[0][0])
|
return frappe.get_doc('Payment Request', pr)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
|
@ -32,6 +32,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
|||||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||||
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||||
@ -107,6 +108,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.validate_uom_is_integer("uom", "qty")
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
self.set_expense_account(for_validate=True)
|
self.set_expense_account(for_validate=True)
|
||||||
|
self.validate_expense_account()
|
||||||
self.set_against_expense_account()
|
self.set_against_expense_account()
|
||||||
self.validate_write_off_account()
|
self.validate_write_off_account()
|
||||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
|
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
|
||||||
@ -311,6 +313,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
elif not item.expense_account and for_validate:
|
elif not item.expense_account and for_validate:
|
||||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||||
|
|
||||||
|
def validate_expense_account(self):
|
||||||
|
for item in self.get('items'):
|
||||||
|
validate_account_head(item.idx, item.expense_account, self.company, 'Expense')
|
||||||
|
|
||||||
def set_against_expense_account(self):
|
def set_against_expense_account(self):
|
||||||
against_accounts = []
|
against_accounts = []
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
|
@ -37,6 +37,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
|||||||
get_gl_entries_on_asset_regain,
|
get_gl_entries_on_asset_regain,
|
||||||
make_depreciation_entry,
|
make_depreciation_entry,
|
||||||
)
|
)
|
||||||
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||||
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
||||||
@ -111,6 +112,8 @@ class SalesInvoice(SellingController):
|
|||||||
self.validate_fixed_asset()
|
self.validate_fixed_asset()
|
||||||
self.set_income_account_for_fixed_assets()
|
self.set_income_account_for_fixed_assets()
|
||||||
self.validate_item_cost_centers()
|
self.validate_item_cost_centers()
|
||||||
|
self.validate_income_account()
|
||||||
|
|
||||||
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
|
validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference)
|
||||||
|
|
||||||
if cint(self.is_pos):
|
if cint(self.is_pos):
|
||||||
@ -178,6 +181,10 @@ class SalesInvoice(SellingController):
|
|||||||
if cost_center_company != self.company:
|
if cost_center_company != self.company:
|
||||||
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
|
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
|
||||||
|
|
||||||
|
def validate_income_account(self):
|
||||||
|
for item in self.get('items'):
|
||||||
|
validate_account_head(item.idx, item.income_account, self.company, 'Income')
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
def set_tax_withholding(self):
|
||||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||||
|
|
||||||
@ -1704,6 +1711,7 @@ def make_delivery_note(source_name, target_doc=None):
|
|||||||
}
|
}
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
|
doclist.set_onload('ignore_price_list', True)
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -267,7 +267,7 @@ def get_loan_amount(filters):
|
|||||||
|
|
||||||
total_amount += flt(amount)
|
total_amount += flt(amount)
|
||||||
|
|
||||||
return amount
|
return total_amount
|
||||||
|
|
||||||
def get_balance_row(label, amount, account_currency):
|
def get_balance_row(label, amount, account_currency):
|
||||||
if amount > 0:
|
if amount > 0:
|
||||||
|
@ -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
|
||||||
|
@ -88,10 +88,12 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
posting_date="2021-05-01",
|
posting_date="2021-05-01",
|
||||||
parent_cost_center="Main - _CD",
|
parent_cost_center="Main - _CD",
|
||||||
cost_center="Main - _CD",
|
cost_center="Main - _CD",
|
||||||
do_not_submit=True,
|
do_not_save=True,
|
||||||
rate=300,
|
rate=300,
|
||||||
price_list_rate=300,
|
price_list_rate=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
si.items[0].income_account = "Sales - _CD"
|
||||||
si.items[0].enable_deferred_revenue = 1
|
si.items[0].enable_deferred_revenue = 1
|
||||||
si.items[0].service_start_date = "2021-05-01"
|
si.items[0].service_start_date = "2021-05-01"
|
||||||
si.items[0].service_end_date = "2021-08-01"
|
si.items[0].service_end_date = "2021-08-01"
|
||||||
@ -269,11 +271,13 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
|||||||
posting_date="2021-05-01",
|
posting_date="2021-05-01",
|
||||||
parent_cost_center="Main - _CD",
|
parent_cost_center="Main - _CD",
|
||||||
cost_center="Main - _CD",
|
cost_center="Main - _CD",
|
||||||
do_not_submit=True,
|
do_not_save=True,
|
||||||
rate=300,
|
rate=300,
|
||||||
price_list_rate=300,
|
price_list_rate=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
si.items[0].enable_deferred_revenue = 1
|
si.items[0].enable_deferred_revenue = 1
|
||||||
|
si.items[0].income_account = "Sales - _CD"
|
||||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||||
si.items[0].income_account = "Sales - _CD"
|
si.items[0].income_account = "Sales - _CD"
|
||||||
si.save()
|
si.save()
|
||||||
|
@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
"label": __("From Date"),
|
"label": __("From Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"default": frappe.defaults.get_user_default("year_start_date"),
|
"default": frappe.defaults.get_user_default("year_start_date"),
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "to_date",
|
"fieldname": "to_date",
|
||||||
"label": __("To Date"),
|
"label": __("To Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"default": frappe.defaults.get_user_default("year_end_date"),
|
"default": frappe.defaults.get_user_default("year_end_date"),
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "finance_book",
|
"fieldname": "finance_book",
|
||||||
@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
"fieldname": "dimension",
|
"fieldname": "dimension",
|
||||||
"label": __("Select Dimension"),
|
"label": __("Select Dimension"),
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
|
"default": "Cost Center",
|
||||||
"options": get_accounting_dimension_options(),
|
"options": get_accounting_dimension_options(),
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
},
|
},
|
||||||
@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function get_accounting_dimension_options() {
|
function get_accounting_dimension_options() {
|
||||||
let options =["", "Cost Center", "Project"];
|
let options =["Cost Center", "Project"];
|
||||||
frappe.db.get_list('Accounting Dimension',
|
frappe.db.get_list('Accounting Dimension',
|
||||||
{fields:['document_type']}).then((res) => {
|
{fields:['document_type']}).then((res) => {
|
||||||
res.forEach((dimension) => {
|
res.forEach((dimension) => {
|
||||||
|
@ -15,20 +15,21 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
|||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
validate_filters(filters)
|
|
||||||
dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
|
|
||||||
|
|
||||||
if not dimension_items_list:
|
validate_filters(filters)
|
||||||
|
dimension_list = get_dimensions(filters)
|
||||||
|
|
||||||
|
if not dimension_list:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
dimension_items_list = [''.join(d) for d in dimension_items_list]
|
columns = get_columns(dimension_list)
|
||||||
columns = get_columns(dimension_items_list)
|
data = get_data(filters, dimension_list)
|
||||||
data = get_data(filters, dimension_items_list)
|
|
||||||
|
|
||||||
return columns, data
|
return columns, data
|
||||||
|
|
||||||
def get_data(filters, dimension_items_list):
|
def get_data(filters, dimension_list):
|
||||||
company_currency = erpnext.get_company_currency(filters.company)
|
company_currency = erpnext.get_company_currency(filters.company)
|
||||||
|
|
||||||
acc = frappe.db.sql("""
|
acc = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
name, account_number, parent_account, lft, rgt, root_type,
|
name, account_number, parent_account, lft, rgt, root_type,
|
||||||
@ -51,60 +52,54 @@ def get_data(filters, dimension_items_list):
|
|||||||
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
|
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
|
||||||
|
|
||||||
gl_entries_by_account = {}
|
gl_entries_by_account = {}
|
||||||
set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
|
set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account)
|
||||||
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
|
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list,
|
||||||
accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
|
frappe.scrub(filters.get('dimension')))
|
||||||
out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
|
accumulate_values_into_parents(accounts, accounts_by_name, dimension_list)
|
||||||
|
out = prepare_data(accounts, filters, company_currency, dimension_list)
|
||||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
|
def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
|
||||||
for item in dimension_items_list:
|
condition = get_condition(filters.get('dimension'))
|
||||||
condition = get_condition(filters.from_date, item, filters.dimension)
|
|
||||||
if account:
|
|
||||||
condition += " and account in ({})"\
|
|
||||||
.format(", ".join([frappe.db.escape(d) for d in account]))
|
|
||||||
|
|
||||||
gl_filters = {
|
if account:
|
||||||
"company": filters.get("company"),
|
condition += " and account in ({})"\
|
||||||
"from_date": filters.get("from_date"),
|
.format(", ".join([frappe.db.escape(d) for d in account]))
|
||||||
"to_date": filters.get("to_date"),
|
|
||||||
"finance_book": cstr(filters.get("finance_book"))
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_filters['item'] = ''.join(item)
|
gl_filters = {
|
||||||
|
"company": filters.get("company"),
|
||||||
|
"from_date": filters.get("from_date"),
|
||||||
|
"to_date": filters.get("to_date"),
|
||||||
|
"finance_book": cstr(filters.get("finance_book"))
|
||||||
|
}
|
||||||
|
|
||||||
if filters.get("include_default_book_entries"):
|
gl_filters['dimensions'] = set(dimension_list)
|
||||||
gl_filters["company_fb"] = frappe.db.get_value("Company",
|
|
||||||
filters.company, 'default_finance_book')
|
|
||||||
|
|
||||||
for key, value in filters.items():
|
if filters.get("include_default_book_entries"):
|
||||||
if value:
|
gl_filters["company_fb"] = frappe.db.get_value("Company",
|
||||||
gl_filters.update({
|
filters.company, 'default_finance_book')
|
||||||
key: value
|
|
||||||
})
|
|
||||||
|
|
||||||
gl_entries = frappe.db.sql("""
|
gl_entries = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
posting_date, account, debit, credit, is_opening, fiscal_year,
|
posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
|
||||||
debit_in_account_currency, credit_in_account_currency, account_currency
|
debit_in_account_currency, credit_in_account_currency, account_currency
|
||||||
from
|
from
|
||||||
`tabGL Entry`
|
`tabGL Entry`
|
||||||
where
|
where
|
||||||
company=%(company)s
|
company=%(company)s
|
||||||
{condition}
|
{condition}
|
||||||
|
and posting_date >= %(from_date)s
|
||||||
and posting_date <= %(to_date)s
|
and posting_date <= %(to_date)s
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
order by account, posting_date""".format(
|
order by account, posting_date""".format(
|
||||||
condition=condition),
|
dimension = frappe.scrub(filters.get('dimension')), condition=condition), gl_filters, as_dict=True) #nosec
|
||||||
gl_filters, as_dict=True) #nosec
|
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
entry['dimension_item'] = ''.join(item)
|
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
|
||||||
|
|
||||||
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
|
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type):
|
||||||
|
|
||||||
for entries in gl_entries_by_account.values():
|
for entries in gl_entries_by_account.values():
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
@ -114,11 +109,12 @@ def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_l
|
|||||||
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
|
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
|
||||||
raise_exception=1
|
raise_exception=1
|
||||||
)
|
)
|
||||||
for item in dimension_items_list:
|
|
||||||
if item == entry.dimension_item:
|
|
||||||
d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
|
|
||||||
|
|
||||||
def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
|
for dimension in dimension_list:
|
||||||
|
if dimension == entry.get(dimension_type):
|
||||||
|
d[frappe.scrub(dimension)] = d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||||
|
|
||||||
|
def prepare_data(accounts, filters, company_currency, dimension_list):
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
@ -135,13 +131,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen
|
|||||||
if d.account_number else d.account_name)
|
if d.account_number else d.account_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
for item in dimension_items_list:
|
for dimension in dimension_list:
|
||||||
row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
|
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||||
|
|
||||||
if abs(row[frappe.scrub(item)]) >= 0.005:
|
if abs(row[frappe.scrub(dimension)]) >= 0.005:
|
||||||
# ignore zero values
|
# ignore zero values
|
||||||
has_value = True
|
has_value = True
|
||||||
total += flt(d.get(frappe.scrub(item), 0.0), 3)
|
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||||
|
|
||||||
row["has_value"] = has_value
|
row["has_value"] = has_value
|
||||||
row["total"] = total
|
row["total"] = total
|
||||||
@ -149,62 +145,55 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
|
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
|
||||||
"""accumulate children's values in parent accounts"""
|
"""accumulate children's values in parent accounts"""
|
||||||
for d in reversed(accounts):
|
for d in reversed(accounts):
|
||||||
if d.parent_account:
|
if d.parent_account:
|
||||||
for item in dimension_items_list:
|
for dimension in dimension_list:
|
||||||
accounts_by_name[d.parent_account][frappe.scrub(item)] = \
|
accounts_by_name[d.parent_account][frappe.scrub(dimension)] = \
|
||||||
accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
|
accounts_by_name[d.parent_account].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
|
||||||
|
|
||||||
def get_condition(from_date, item, dimension):
|
def get_condition(dimension):
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
if from_date:
|
conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension)))
|
||||||
conditions.append("posting_date >= %(from_date)s")
|
|
||||||
if dimension:
|
|
||||||
if dimension not in ['Cost Center', 'Project']:
|
|
||||||
if dimension in ['Customer', 'Supplier']:
|
|
||||||
dimension = 'Party'
|
|
||||||
else:
|
|
||||||
dimension = 'Voucher No'
|
|
||||||
txt = "{0} = %(item)s".format(frappe.scrub(dimension))
|
|
||||||
conditions.append(txt)
|
|
||||||
|
|
||||||
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||||
|
|
||||||
def get_dimension_items_list(dimension, company):
|
def get_dimensions(filters):
|
||||||
meta = frappe.get_meta(dimension, cached=False)
|
meta = frappe.get_meta(filters.get('dimension'), cached=False)
|
||||||
fieldnames = [d.fieldname for d in meta.get("fields")]
|
query_filters = {}
|
||||||
filters = {}
|
|
||||||
if 'company' in fieldnames:
|
|
||||||
filters['company'] = company
|
|
||||||
return frappe.get_all(dimension, filters, as_list=True)
|
|
||||||
|
|
||||||
def get_columns(dimension_items_list, accumulated_values=1, company=None):
|
if meta.has_field('company'):
|
||||||
|
query_filters = {'company': filters.get('company')}
|
||||||
|
|
||||||
|
return frappe.get_all(filters.get('dimension'), filters=query_filters, pluck='name')
|
||||||
|
|
||||||
|
def get_columns(dimension_list):
|
||||||
columns = [{
|
columns = [{
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
"label": _("Account"),
|
"label": _("Account"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"width": 300
|
"width": 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"label": _("Currency"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Currency",
|
||||||
|
"hidden": 1
|
||||||
}]
|
}]
|
||||||
if company:
|
|
||||||
|
for dimension in dimension_list:
|
||||||
columns.append({
|
columns.append({
|
||||||
"fieldname": "currency",
|
"fieldname": frappe.scrub(dimension),
|
||||||
"label": _("Currency"),
|
"label": dimension,
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Currency",
|
|
||||||
"hidden": 1
|
|
||||||
})
|
|
||||||
for item in dimension_items_list:
|
|
||||||
columns.append({
|
|
||||||
"fieldname": frappe.scrub(item),
|
|
||||||
"label": item,
|
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 150
|
"width": 150
|
||||||
})
|
})
|
||||||
|
|
||||||
columns.append({
|
columns.append({
|
||||||
"fieldname": "total",
|
"fieldname": "total",
|
||||||
"label": "Total",
|
"label": "Total",
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -442,6 +442,8 @@ def make_purchase_receipt(source_name, target_doc=None):
|
|||||||
}
|
}
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
|
doc.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -509,6 +511,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
|
|
||||||
doc = get_mapped_doc("Purchase Order", source_name, fields,
|
doc = get_mapped_doc("Purchase Order", source_name, fields,
|
||||||
target_doc, postprocess, ignore_permissions=ignore_permissions)
|
target_doc, postprocess, ignore_permissions=ignore_permissions)
|
||||||
|
doc.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
@ -139,6 +139,7 @@ def make_purchase_order(source_name, target_doc=None):
|
|||||||
},
|
},
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
|
doclist.set_onload('ignore_price_list', True)
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -1566,12 +1566,12 @@ def validate_taxes_and_charges(tax):
|
|||||||
tax.rate = None
|
tax.rate = None
|
||||||
|
|
||||||
|
|
||||||
def validate_account_head(idx, account, company):
|
def validate_account_head(idx, account, company, context=''):
|
||||||
account_company = frappe.get_cached_value('Account', account, 'company')
|
account_company = frappe.get_cached_value('Account', account, 'company')
|
||||||
|
|
||||||
if account_company != company:
|
if account_company != company:
|
||||||
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
|
frappe.throw(_('Row {0}: {3} Account {1} does not belong to Company {2}')
|
||||||
.format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
|
.format(idx, frappe.bold(account), frappe.bold(company), context), title=_('Invalid Account'))
|
||||||
|
|
||||||
|
|
||||||
def validate_cost_center(tax, doc):
|
def validate_cost_center(tax, doc):
|
||||||
|
@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{account_type_condition}
|
{account_type_condition}
|
||||||
AND is_group = 0
|
AND is_group = 0
|
||||||
AND company = %(company)s
|
AND company = %(company)s
|
||||||
AND account_currency = %(currency)s
|
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
||||||
AND `{searchfield}` LIKE %(txt)s
|
AND `{searchfield}` LIKE %(txt)s
|
||||||
{mcond}
|
{mcond}
|
||||||
ORDER BY idx DESC, name
|
ORDER BY idx DESC, name
|
||||||
|
@ -225,9 +225,7 @@ def _check_agent_availability(agent_email, scheduled_time):
|
|||||||
|
|
||||||
|
|
||||||
def _get_employee_from_user(user):
|
def _get_employee_from_user(user):
|
||||||
employee_docname = frappe.db.exists(
|
employee_docname = frappe.db.get_value('Employee', {'user_id': user})
|
||||||
{'doctype': 'Employee', 'user_id': user})
|
|
||||||
if employee_docname:
|
if employee_docname:
|
||||||
# frappe.db.exists returns a tuple of a tuple
|
return frappe.get_doc('Employee', employee_docname)
|
||||||
return frappe.get_doc('Employee', employee_docname[0][0])
|
|
||||||
return None
|
return None
|
||||||
|
@ -8,50 +8,44 @@ import frappe
|
|||||||
|
|
||||||
|
|
||||||
def create_test_lead():
|
def create_test_lead():
|
||||||
test_lead = frappe.db.exists({'doctype': 'Lead', 'email_id':'test@example.com'})
|
test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"})
|
||||||
if test_lead:
|
if test_lead:
|
||||||
return frappe.get_doc('Lead', test_lead[0][0])
|
return frappe.get_doc("Lead", test_lead)
|
||||||
test_lead = frappe.get_doc({
|
test_lead = frappe.get_doc(
|
||||||
'doctype': 'Lead',
|
{"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"}
|
||||||
'lead_name': 'Test Lead',
|
)
|
||||||
'email_id': 'test@example.com'
|
test_lead.insert(ignore_permissions=True)
|
||||||
})
|
return test_lead
|
||||||
test_lead.insert(ignore_permissions=True)
|
|
||||||
return test_lead
|
|
||||||
|
|
||||||
|
|
||||||
def create_test_appointments():
|
def create_test_appointments():
|
||||||
test_appointment = frappe.db.exists(
|
test_appointment = frappe.get_doc(
|
||||||
{'doctype': 'Appointment', 'scheduled_time':datetime.datetime.now(),'email':'test@example.com'})
|
{
|
||||||
if test_appointment:
|
"doctype": "Appointment",
|
||||||
return frappe.get_doc('Appointment', test_appointment[0][0])
|
"email": "test@example.com",
|
||||||
test_appointment = frappe.get_doc({
|
"status": "Open",
|
||||||
'doctype': 'Appointment',
|
"customer_name": "Test Lead",
|
||||||
'email': 'test@example.com',
|
"customer_phone_number": "666",
|
||||||
'status': 'Open',
|
"customer_skype": "test",
|
||||||
'customer_name': 'Test Lead',
|
"customer_email": "test@example.com",
|
||||||
'customer_phone_number': '666',
|
"scheduled_time": datetime.datetime.now(),
|
||||||
'customer_skype': 'test',
|
}
|
||||||
'customer_email': 'test@example.com',
|
)
|
||||||
'scheduled_time': datetime.datetime.now()
|
test_appointment.insert()
|
||||||
})
|
return test_appointment
|
||||||
test_appointment.insert()
|
|
||||||
return test_appointment
|
|
||||||
|
|
||||||
|
|
||||||
class TestAppointment(unittest.TestCase):
|
class TestAppointment(unittest.TestCase):
|
||||||
test_appointment = test_lead = None
|
test_appointment = test_lead = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.test_lead = create_test_lead()
|
self.test_lead = create_test_lead()
|
||||||
self.test_appointment = create_test_appointments()
|
self.test_appointment = create_test_appointments()
|
||||||
|
|
||||||
def test_calendar_event_created(self):
|
def test_calendar_event_created(self):
|
||||||
cal_event = frappe.get_doc(
|
cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event)
|
||||||
'Event', self.test_appointment.calendar_event)
|
self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time)
|
||||||
self.assertEqual(cal_event.starts_on,
|
|
||||||
self.test_appointment.scheduled_time)
|
|
||||||
|
|
||||||
def test_lead_linked(self):
|
def test_lead_linked(self):
|
||||||
lead = frappe.get_doc('Lead', self.test_lead.name)
|
lead = frappe.get_doc("Lead", self.test_lead.name)
|
||||||
self.assertIsNotNone(lead)
|
self.assertIsNotNone(lead)
|
||||||
|
@ -418,6 +418,22 @@ erpnext.ProductView = class {
|
|||||||
|
|
||||||
me.change_route_with_filters();
|
me.change_route_with_filters();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bind filter lookup input box
|
||||||
|
$('.filter-lookup-input').on('keydown', frappe.utils.debounce((e) => {
|
||||||
|
const $input = $(e.target);
|
||||||
|
const keyword = ($input.val() || '').toLowerCase();
|
||||||
|
const $filter_options = $input.next('.filter-options');
|
||||||
|
|
||||||
|
$filter_options.find('.filter-lookup-wrapper').show();
|
||||||
|
$filter_options.find('.filter-lookup-wrapper').each((i, el) => {
|
||||||
|
const $el = $(el);
|
||||||
|
const value = $el.data('value').toLowerCase();
|
||||||
|
if (!value.includes(keyword)) {
|
||||||
|
$el.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
change_route_with_filters() {
|
change_route_with_filters() {
|
||||||
|
@ -15,6 +15,7 @@ class TestVariantSelector(FrappeTestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
template_item = make_item("Test-Tshirt-Temp", {
|
template_item = make_item("Test-Tshirt-Temp", {
|
||||||
"has_variant": 1,
|
"has_variant": 1,
|
||||||
"variant_based_on": "Item Attribute",
|
"variant_based_on": "Item Attribute",
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from erpnext.setup.utils import insert_record
|
|
||||||
|
|
||||||
|
|
||||||
def setup_education():
|
def setup_education():
|
||||||
@ -13,6 +12,21 @@ def setup_education():
|
|||||||
return
|
return
|
||||||
create_academic_sessions()
|
create_academic_sessions()
|
||||||
|
|
||||||
|
|
||||||
|
def insert_record(records):
|
||||||
|
for r in records:
|
||||||
|
doc = frappe.new_doc(r.get("doctype"))
|
||||||
|
doc.update(r)
|
||||||
|
try:
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
except frappe.DuplicateEntryError as e:
|
||||||
|
# pass DuplicateEntryError and continue
|
||||||
|
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
|
||||||
|
# make sure DuplicateEntryError is for the exact same doc and not a related doc
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def create_academic_sessions():
|
def create_academic_sessions():
|
||||||
data = [
|
data = [
|
||||||
{"doctype": "Academic Year", "academic_year_name": "2015-16"},
|
{"doctype": "Academic Year", "academic_year_name": "2015-16"},
|
||||||
|
@ -25,6 +25,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
|
|||||||
LeaveDayBlockedError,
|
LeaveDayBlockedError,
|
||||||
NotAnOptionalHoliday,
|
NotAnOptionalHoliday,
|
||||||
OverlapError,
|
OverlapError,
|
||||||
|
get_leave_allocation_records,
|
||||||
get_leave_balance_on,
|
get_leave_balance_on,
|
||||||
get_leave_details,
|
get_leave_details,
|
||||||
)
|
)
|
||||||
@ -882,6 +883,27 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
|
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
|
||||||
self.assertEqual(leave_allocation['remaining_leaves'], 26)
|
self.assertEqual(leave_allocation['remaining_leaves'], 26)
|
||||||
|
|
||||||
|
@set_holiday_list('Salary Slip Test Holiday List', '_Test Company')
|
||||||
|
def test_get_leave_allocation_records(self):
|
||||||
|
employee = get_employee()
|
||||||
|
leave_type = create_leave_type(
|
||||||
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
|
is_carry_forward=1,
|
||||||
|
expire_carry_forwarded_leaves_after_days=90)
|
||||||
|
leave_type.insert()
|
||||||
|
|
||||||
|
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
|
||||||
|
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
|
||||||
|
expected_data = {
|
||||||
|
"from_date": getdate(leave_alloc.from_date),
|
||||||
|
"to_date": getdate(leave_alloc.to_date),
|
||||||
|
"total_leaves_allocated": 30.0,
|
||||||
|
"unused_leaves": 15.0,
|
||||||
|
"new_leaves_allocated": 15.0,
|
||||||
|
"leave_type": leave_type.name
|
||||||
|
}
|
||||||
|
self.assertEqual(details.get(leave_type.name), expected_data)
|
||||||
|
|
||||||
|
|
||||||
def create_carry_forwarded_allocation(employee, leave_type):
|
def create_carry_forwarded_allocation(employee, leave_type):
|
||||||
# initial leave allocation
|
# initial leave allocation
|
||||||
@ -903,6 +925,8 @@ def create_carry_forwarded_allocation(employee, leave_type):
|
|||||||
carry_forward=1)
|
carry_forward=1)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
|
||||||
|
return leave_allocation
|
||||||
|
|
||||||
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
|
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None):
|
||||||
allocation = frappe.get_doc({
|
allocation = frappe.get_doc({
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
@ -931,12 +955,9 @@ def set_leave_approver():
|
|||||||
dept_doc.save(ignore_permissions=True)
|
dept_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
def get_leave_period():
|
def get_leave_period():
|
||||||
leave_period_name = frappe.db.exists({
|
leave_period_name = frappe.db.get_value("Leave Period", {"company": "_Test Company"})
|
||||||
"doctype": "Leave Period",
|
|
||||||
"company": "_Test Company"
|
|
||||||
})
|
|
||||||
if leave_period_name:
|
if leave_period_name:
|
||||||
return frappe.get_doc("Leave Period", leave_period_name[0][0])
|
return frappe.get_doc("Leave Period", leave_period_name)
|
||||||
else:
|
else:
|
||||||
return frappe.get_doc(dict(
|
return frappe.get_doc(dict(
|
||||||
name = 'Test Leave Period',
|
name = 'Test Leave Period',
|
||||||
|
@ -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:
|
||||||
|
@ -500,6 +500,9 @@ class JobCard(Document):
|
|||||||
2: "Cancelled"
|
2: "Cancelled"
|
||||||
}[self.docstatus or 0]
|
}[self.docstatus or 0]
|
||||||
|
|
||||||
|
if self.for_quantity <= self.transferred_qty:
|
||||||
|
self.status = 'Material Transferred'
|
||||||
|
|
||||||
if self.time_logs:
|
if self.time_logs:
|
||||||
self.status = 'Work In Progress'
|
self.status = 'Work In Progress'
|
||||||
|
|
||||||
@ -507,10 +510,6 @@ class JobCard(Document):
|
|||||||
(self.for_quantity <= self.total_completed_qty or not self.items)):
|
(self.for_quantity <= self.total_completed_qty or not self.items)):
|
||||||
self.status = 'Completed'
|
self.status = 'Completed'
|
||||||
|
|
||||||
if self.status != 'Completed':
|
|
||||||
if self.for_quantity <= self.transferred_qty:
|
|
||||||
self.status = 'Material Transferred'
|
|
||||||
|
|
||||||
if update_status:
|
if update_status:
|
||||||
self.db_set('status', self.status)
|
self.db_set('status', self.status)
|
||||||
|
|
||||||
|
@ -169,6 +169,7 @@ class TestJobCard(FrappeTestCase):
|
|||||||
|
|
||||||
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
|
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
|
||||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||||
|
self.assertEqual(job_card.status, "Open")
|
||||||
|
|
||||||
# fully transfer both RMs
|
# fully transfer both RMs
|
||||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||||
|
@ -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:
|
||||||
|
@ -1150,6 +1150,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
|||||||
doc.insert()
|
doc.insert()
|
||||||
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
|
frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
|
||||||
|
|
||||||
|
if enable_capacity_planning:
|
||||||
|
# automatically added scheduling rows shouldn't change status to WIP
|
||||||
|
doc.db_set("status", "Open")
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
def get_work_order_operation_data(work_order, operation, workstation):
|
def get_work_order_operation_data(work_order, operation, workstation):
|
||||||
|
@ -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()
|
@ -708,6 +708,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr
|
|||||||
if not_submitted_ss:
|
if not_submitted_ss:
|
||||||
frappe.msgprint(_("Could not submit some Salary Slips"))
|
frappe.msgprint(_("Could not submit some Salary Slips"))
|
||||||
|
|
||||||
|
frappe.flags.via_payroll_entry = False
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):
|
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
|
|||||||
class TestSalarySlip(unittest.TestCase):
|
class TestSalarySlip(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
setup_test()
|
setup_test()
|
||||||
|
frappe.flags.pop("via_payroll_entry", None)
|
||||||
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@ -409,15 +411,17 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"email_salary_slip_to_employee": 1
|
"email_salary_slip_to_employee": 1
|
||||||
})
|
})
|
||||||
def test_email_salary_slip(self):
|
def test_email_salary_slip(self):
|
||||||
frappe.db.sql("delete from `tabEmail Queue`")
|
frappe.db.delete("Email Queue")
|
||||||
|
|
||||||
make_employee("test_email_salary_slip@salary.com", company="_Test Company")
|
user_id = "test_email_salary_slip@salary.com"
|
||||||
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
|
||||||
|
make_employee(user_id, company="_Test Company")
|
||||||
|
ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email")
|
||||||
ss.company = "_Test Company"
|
ss.company = "_Test Company"
|
||||||
ss.save()
|
ss.save()
|
||||||
ss.submit()
|
ss.submit()
|
||||||
|
|
||||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
|
email_queue = frappe.db.a_row_exists("Email Queue")
|
||||||
self.assertTrue(email_queue)
|
self.assertTrue(email_queue)
|
||||||
|
|
||||||
def test_loan_repayment_salary_slip(self):
|
def test_loan_repayment_salary_slip(self):
|
||||||
|
@ -1070,7 +1070,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(flt(this.frm.doc.conversion_rate)>0.0) {
|
if(flt(this.frm.doc.conversion_rate)>0.0) {
|
||||||
if(this.frm.doc.ignore_pricing_rule) {
|
if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) {
|
||||||
this.calculate_taxes_and_totals();
|
this.calculate_taxes_and_totals();
|
||||||
} else if (!this.in_apply_price_list){
|
} else if (!this.in_apply_price_list){
|
||||||
this.apply_price_list();
|
this.apply_price_list();
|
||||||
@ -1884,6 +1884,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc) {
|
if(!r.exc) {
|
||||||
item.item_tax_rate = r.message;
|
item.item_tax_rate = r.message;
|
||||||
|
me.add_taxes_from_item_tax_template(item.item_tax_rate);
|
||||||
me.calculate_taxes_and_totals();
|
me.calculate_taxes_and_totals();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,21 +579,40 @@ 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
|
||||||
&& doc.fg_completed_qty
|
&& doc.fg_completed_qty
|
||||||
&& erpnext.stock.bom
|
&& erpnext.stock.bom
|
||||||
&& erpnext.stock.bom.name === doc.bom_no;
|
&& erpnext.stock.bom.name === doc.bom_no;
|
||||||
const itemChecks = !!item && !item.allow_alternative_item;
|
const itemChecks = !!item
|
||||||
|
&& !item.allow_alternative_item
|
||||||
|
&& erpnext.stock.bom && erpnext.stock.items
|
||||||
|
&& (item.item_code in erpnext.stock.bom.items);
|
||||||
return docChecks && itemChecks;
|
return docChecks && itemChecks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//# sourceURL=serial_no_batch_selector.js
|
||||||
|
@ -264,6 +264,15 @@ body.product-page {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-lookup-input {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
@ -19,8 +19,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
|
|||||||
|
|
||||||
|
|
||||||
def validate_gstin_for_india(doc, method):
|
def validate_gstin_for_india(doc, method):
|
||||||
if hasattr(doc, 'gst_state') and doc.gst_state:
|
if hasattr(doc, 'gst_state'):
|
||||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
set_gst_state_and_state_number(doc)
|
||||||
|
|
||||||
if not hasattr(doc, 'gstin') or not doc.gstin:
|
if not hasattr(doc, 'gstin') or not doc.gstin:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -50,7 +51,6 @@ def validate_gstin_for_india(doc, method):
|
|||||||
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
|
frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN"))
|
||||||
|
|
||||||
validate_gstin_check_digit(doc.gstin)
|
validate_gstin_check_digit(doc.gstin)
|
||||||
set_gst_state_and_state_number(doc)
|
|
||||||
|
|
||||||
if not doc.gst_state:
|
if not doc.gst_state:
|
||||||
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
|
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
|
||||||
@ -82,17 +82,14 @@ def update_gst_category(doc, method):
|
|||||||
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
|
frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular')
|
||||||
|
|
||||||
def set_gst_state_and_state_number(doc):
|
def set_gst_state_and_state_number(doc):
|
||||||
if not doc.gst_state:
|
if not doc.gst_state and doc.state:
|
||||||
if not doc.state:
|
|
||||||
return
|
|
||||||
state = doc.state.lower()
|
state = doc.state.lower()
|
||||||
states_lowercase = {s.lower():s for s in states}
|
states_lowercase = {s.lower():s for s in states}
|
||||||
if state in states_lowercase:
|
if state in states_lowercase:
|
||||||
doc.gst_state = states_lowercase[state]
|
doc.gst_state = states_lowercase[state]
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
doc.gst_state_number = state_numbers.get(doc.gst_state)
|
||||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
|
||||||
|
|
||||||
def validate_gstin_check_digit(gstin, label='GSTIN'):
|
def validate_gstin_check_digit(gstin, label='GSTIN'):
|
||||||
''' Function to validate the check digit of the GSTIN.'''
|
''' Function to validate the check digit of the GSTIN.'''
|
||||||
|
@ -206,6 +206,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
|
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
|
||||||
|
|
||||||
# postprocess: fetch shipping address, set missing values
|
# postprocess: fetch shipping address, set missing values
|
||||||
|
doclist.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
@ -269,6 +270,8 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
}
|
}
|
||||||
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
|
}, target_doc, set_missing_values, ignore_permissions=ignore_permissions)
|
||||||
|
|
||||||
|
doclist.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
def _make_customer(source_name, ignore_permissions=False):
|
def _make_customer(source_name, ignore_permissions=False):
|
||||||
|
@ -130,6 +130,7 @@
|
|||||||
"per_delivered",
|
"per_delivered",
|
||||||
"column_break_81",
|
"column_break_81",
|
||||||
"per_billed",
|
"per_billed",
|
||||||
|
"per_picked",
|
||||||
"billing_status",
|
"billing_status",
|
||||||
"sales_team_section_break",
|
"sales_team_section_break",
|
||||||
"sales_partner",
|
"sales_partner",
|
||||||
@ -1514,13 +1515,19 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount Eligible for Commission",
|
"label": "Amount Eligible for Commission",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "per_picked",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"label": "% Picked",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-05 12:16:40.775704",
|
"modified": "2022-03-15 21:38:31.437586",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
@ -1594,6 +1601,7 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"timeline_field": "customer",
|
"timeline_field": "customer",
|
||||||
"title_field": "customer_name",
|
"title_field": "customer_name",
|
||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
|
@ -584,6 +584,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
|
|||||||
|
|
||||||
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
|
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
|
||||||
|
|
||||||
|
target_doc.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -664,6 +666,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
|||||||
if automatically_fetch_payment_terms:
|
if automatically_fetch_payment_terms:
|
||||||
doclist.set_payment_schedule()
|
doclist.set_payment_schedule()
|
||||||
|
|
||||||
|
doclist.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"quantity_and_rate",
|
"quantity_and_rate",
|
||||||
"qty",
|
"qty",
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
|
"picked_qty",
|
||||||
"col_break2",
|
"col_break2",
|
||||||
"uom",
|
"uom",
|
||||||
"conversion_factor",
|
"conversion_factor",
|
||||||
@ -798,12 +799,17 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Grant Commission",
|
"label": "Grant Commission",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "picked_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Picked Qty"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-02-24 14:41:57.325799",
|
"modified": "2022-03-15 20:17:33.984799",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
frappe.ui.form.on('Sales Person', {
|
frappe.ui.form.on('Sales Person', {
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
|
if(frm.doc.__onload && frm.doc.__onload.dashboard_info) {
|
||||||
var info = frm.doc.__onload.dashboard_info;
|
let info = frm.doc.__onload.dashboard_info;
|
||||||
frm.dashboard.add_indicator(__('Total Contribution Amount: {0}', [format_currency(info.allocated_amount, info.currency)]), 'blue');
|
frm.dashboard.add_indicator(__('Total Contribution Amount Against Orders: {0}',
|
||||||
|
[format_currency(info.allocated_amount_against_order, info.currency)]), 'blue');
|
||||||
|
|
||||||
|
frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}',
|
||||||
|
[format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -28,14 +28,17 @@ class SalesPerson(NestedSet):
|
|||||||
def load_dashboard_info(self):
|
def load_dashboard_info(self):
|
||||||
company_default_currency = get_default_currency()
|
company_default_currency = get_default_currency()
|
||||||
|
|
||||||
allocated_amount = frappe.db.sql("""
|
allocated_amount_against_order = flt(frappe.db.get_value('Sales Team',
|
||||||
select sum(allocated_amount)
|
{'docstatus': 1, 'parenttype': 'Sales Order', 'sales_person': self.sales_person_name},
|
||||||
from `tabSales Team`
|
'sum(allocated_amount)'))
|
||||||
where sales_person = %s and docstatus=1 and parenttype = 'Sales Order'
|
|
||||||
""",(self.sales_person_name))
|
allocated_amount_against_invoice = flt(frappe.db.get_value('Sales Team',
|
||||||
|
{'docstatus': 1, 'parenttype': 'Sales Invoice', 'sales_person': self.sales_person_name},
|
||||||
|
'sum(allocated_amount)'))
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0
|
info["allocated_amount_against_order"] = allocated_amount_against_order
|
||||||
|
info["allocated_amount_against_invoice"] = allocated_amount_against_invoice
|
||||||
info["currency"] = company_default_currency
|
info["currency"] = company_default_currency
|
||||||
|
|
||||||
self.set_onload('dashboard_info', info)
|
self.set_onload('dashboard_info', info)
|
||||||
|
@ -21,9 +21,7 @@ default_mail_footer = """<div style="padding: 7px; text-align: right; color: #88
|
|||||||
def after_install():
|
def after_install():
|
||||||
frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert()
|
frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert()
|
||||||
set_single_defaults()
|
set_single_defaults()
|
||||||
create_compact_item_print_custom_field()
|
create_print_setting_custom_fields()
|
||||||
create_print_uom_after_qty_custom_field()
|
|
||||||
create_print_zero_amount_taxes_custom_field()
|
|
||||||
add_all_roles_to("Administrator")
|
add_all_roles_to("Administrator")
|
||||||
create_default_cash_flow_mapper_templates()
|
create_default_cash_flow_mapper_templates()
|
||||||
create_default_success_action()
|
create_default_success_action()
|
||||||
@ -77,7 +75,7 @@ def setup_currency_exchange():
|
|||||||
except frappe.ValidationError:
|
except frappe.ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def create_compact_item_print_custom_field():
|
def create_print_setting_custom_fields():
|
||||||
create_custom_field('Print Settings', {
|
create_custom_field('Print Settings', {
|
||||||
'label': _('Compact Item Print'),
|
'label': _('Compact Item Print'),
|
||||||
'fieldname': 'compact_item_print',
|
'fieldname': 'compact_item_print',
|
||||||
@ -85,9 +83,6 @@ def create_compact_item_print_custom_field():
|
|||||||
'default': 1,
|
'default': 1,
|
||||||
'insert_after': 'with_letterhead'
|
'insert_after': 'with_letterhead'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def create_print_uom_after_qty_custom_field():
|
|
||||||
create_custom_field('Print Settings', {
|
create_custom_field('Print Settings', {
|
||||||
'label': _('Print UOM after Quantity'),
|
'label': _('Print UOM after Quantity'),
|
||||||
'fieldname': 'print_uom_after_quantity',
|
'fieldname': 'print_uom_after_quantity',
|
||||||
@ -95,9 +90,6 @@ def create_print_uom_after_qty_custom_field():
|
|||||||
'default': 0,
|
'default': 0,
|
||||||
'insert_after': 'compact_item_print'
|
'insert_after': 'compact_item_print'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def create_print_zero_amount_taxes_custom_field():
|
|
||||||
create_custom_field('Print Settings', {
|
create_custom_field('Print Settings', {
|
||||||
'label': _('Print taxes with zero amount'),
|
'label': _('Print taxes with zero amount'),
|
||||||
'fieldname': 'print_taxes_with_zero_amount',
|
'fieldname': 'print_taxes_with_zero_amount',
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"add_sample_data": 1,
|
|
||||||
"bank_account": "HDFC",
|
|
||||||
"company_abbr": "FT",
|
|
||||||
"company_name": "For Testing",
|
|
||||||
"company_tagline": "Just for GST",
|
|
||||||
"country": "India",
|
|
||||||
"currency": "INR",
|
|
||||||
"customer_1": "Test Customer 1",
|
|
||||||
"customer_2": "Test Customer 2",
|
|
||||||
"domains": ["Manufacturing"],
|
|
||||||
"email": "great@example.com",
|
|
||||||
"full_name": "Great Tester",
|
|
||||||
"fy_end_date": "2018-03-31",
|
|
||||||
"fy_start_date": "2017-04-01",
|
|
||||||
"is_purchase_item_1": 1,
|
|
||||||
"is_purchase_item_2": 1,
|
|
||||||
"is_purchase_item_3": 0,
|
|
||||||
"is_purchase_item_4": 0,
|
|
||||||
"is_purchase_item_5": 0,
|
|
||||||
"is_sales_item_1": 1,
|
|
||||||
"is_sales_item_2": 1,
|
|
||||||
"is_sales_item_3": 1,
|
|
||||||
"is_sales_item_4": 1,
|
|
||||||
"is_sales_item_5": 1,
|
|
||||||
"item_1": "Test Item 1",
|
|
||||||
"item_2": "Test Item 2",
|
|
||||||
"item_group_1": "Products",
|
|
||||||
"item_group_2": "Products",
|
|
||||||
"item_group_3": "Products",
|
|
||||||
"item_group_4": "Products",
|
|
||||||
"item_group_5": "Products",
|
|
||||||
"item_uom_1": "Unit",
|
|
||||||
"item_uom_2": "Unit",
|
|
||||||
"item_uom_3": "Unit",
|
|
||||||
"item_uom_4": "Unit",
|
|
||||||
"item_uom_5": "Unit",
|
|
||||||
"language": "English (United States)",
|
|
||||||
"password": "test",
|
|
||||||
"setup_website": 1,
|
|
||||||
"supplier_1": "Test Supplier 1",
|
|
||||||
"supplier_2": "Test Supplier 2",
|
|
||||||
"timezone": "Asia/Kolkata",
|
|
||||||
"user_accountant_1": 1,
|
|
||||||
"user_accountant_2": 1,
|
|
||||||
"user_accountant_3": 1,
|
|
||||||
"user_accountant_4": 1,
|
|
||||||
"user_purchaser_1": 1,
|
|
||||||
"user_purchaser_2": 1,
|
|
||||||
"user_purchaser_3": 1,
|
|
||||||
"user_purchaser_4": 1,
|
|
||||||
"user_sales_1": 1,
|
|
||||||
"user_sales_2": 1,
|
|
||||||
"user_sales_3": 1,
|
|
||||||
"user_sales_4": 1
|
|
||||||
}
|
|
@ -1,179 +0,0 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
import frappe.utils
|
|
||||||
from frappe import _
|
|
||||||
from frappe.utils.make_random import add_random_children
|
|
||||||
|
|
||||||
|
|
||||||
def make_sample_data(domains, make_dependent = False):
|
|
||||||
"""Create a few opportunities, quotes, material requests, issues, todos, projects
|
|
||||||
to help the user get started"""
|
|
||||||
|
|
||||||
if make_dependent:
|
|
||||||
items = frappe.get_all("Item", {'is_sales_item': 1})
|
|
||||||
customers = frappe.get_all("Customer")
|
|
||||||
warehouses = frappe.get_all("Warehouse")
|
|
||||||
|
|
||||||
if items and customers:
|
|
||||||
for i in range(3):
|
|
||||||
customer = random.choice(customers).name
|
|
||||||
make_opportunity(items, customer)
|
|
||||||
make_quote(items, customer)
|
|
||||||
|
|
||||||
if items and warehouses:
|
|
||||||
make_material_request(frappe.get_all("Item"))
|
|
||||||
|
|
||||||
make_projects(domains)
|
|
||||||
import_notification()
|
|
||||||
|
|
||||||
def make_opportunity(items, customer):
|
|
||||||
b = frappe.get_doc({
|
|
||||||
"doctype": "Opportunity",
|
|
||||||
"opportunity_from": "Customer",
|
|
||||||
"customer": customer,
|
|
||||||
"opportunity_type": _("Sales"),
|
|
||||||
"with_items": 1
|
|
||||||
})
|
|
||||||
|
|
||||||
add_random_children(b, "items", rows=len(items), randomize = {
|
|
||||||
"qty": (1, 5),
|
|
||||||
"item_code": ["Item"]
|
|
||||||
}, unique="item_code")
|
|
||||||
|
|
||||||
b.insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
b.add_comment('Comment', text="This is a dummy record")
|
|
||||||
|
|
||||||
def make_quote(items, customer):
|
|
||||||
qtn = frappe.get_doc({
|
|
||||||
"doctype": "Quotation",
|
|
||||||
"quotation_to": "Customer",
|
|
||||||
"party_name": customer,
|
|
||||||
"order_type": "Sales"
|
|
||||||
})
|
|
||||||
|
|
||||||
add_random_children(qtn, "items", rows=len(items), randomize = {
|
|
||||||
"qty": (1, 5),
|
|
||||||
"item_code": ["Item"]
|
|
||||||
}, unique="item_code")
|
|
||||||
|
|
||||||
qtn.insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
qtn.add_comment('Comment', text="This is a dummy record")
|
|
||||||
|
|
||||||
def make_material_request(items):
|
|
||||||
for i in items:
|
|
||||||
mr = frappe.get_doc({
|
|
||||||
"doctype": "Material Request",
|
|
||||||
"material_request_type": "Purchase",
|
|
||||||
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
|
|
||||||
"items": [{
|
|
||||||
"schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7),
|
|
||||||
"item_code": i.name,
|
|
||||||
"qty": 10
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
mr.insert()
|
|
||||||
mr.submit()
|
|
||||||
|
|
||||||
mr.add_comment('Comment', text="This is a dummy record")
|
|
||||||
|
|
||||||
|
|
||||||
def make_issue():
|
|
||||||
pass
|
|
||||||
|
|
||||||
def make_projects(domains):
|
|
||||||
current_date = frappe.utils.nowdate()
|
|
||||||
project = frappe.get_doc({
|
|
||||||
"doctype": "Project",
|
|
||||||
"project_name": "ERPNext Implementation",
|
|
||||||
})
|
|
||||||
|
|
||||||
tasks = [
|
|
||||||
{
|
|
||||||
"title": "Explore ERPNext",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": current_date,
|
|
||||||
"file": "explore.md"
|
|
||||||
}]
|
|
||||||
|
|
||||||
if 'Education' in domains:
|
|
||||||
tasks += [
|
|
||||||
{
|
|
||||||
"title": _("Setup your Institute in ERPNext"),
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 1),
|
|
||||||
"file": "education_masters.md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Setup Master Data",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 1),
|
|
||||||
"file": "education_masters.md"
|
|
||||||
}]
|
|
||||||
|
|
||||||
else:
|
|
||||||
tasks += [
|
|
||||||
{
|
|
||||||
"title": "Setup Your Company",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 1),
|
|
||||||
"file": "masters.md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Start Tracking your Sales",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 2),
|
|
||||||
"file": "sales.md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Start Managing Purchases",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 3),
|
|
||||||
"file": "purchase.md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Import Data",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 4),
|
|
||||||
"file": "import_data.md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Go Live!",
|
|
||||||
"start_date": current_date,
|
|
||||||
"end_date": frappe.utils.add_days(current_date, 5),
|
|
||||||
"file": "go_live.md"
|
|
||||||
}]
|
|
||||||
|
|
||||||
for t in tasks:
|
|
||||||
with open (os.path.join(os.path.dirname(__file__), "tasks", t['file'])) as f:
|
|
||||||
t['description'] = frappe.utils.md_to_html(f.read())
|
|
||||||
del t['file']
|
|
||||||
|
|
||||||
project.append('tasks', t)
|
|
||||||
|
|
||||||
project.insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
def import_notification():
|
|
||||||
'''Import notification for task start'''
|
|
||||||
with open (os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f:
|
|
||||||
notification = frappe.get_doc(json.loads(f.read())[0])
|
|
||||||
notification.insert()
|
|
||||||
|
|
||||||
# trigger the first message!
|
|
||||||
from frappe.email.doctype.notification.notification import trigger_daily_alerts
|
|
||||||
trigger_daily_alerts()
|
|
||||||
|
|
||||||
def test_sample():
|
|
||||||
frappe.db.sql('delete from `tabNotification`')
|
|
||||||
frappe.db.sql('delete from tabProject')
|
|
||||||
frappe.db.sql('delete from tabTask')
|
|
||||||
make_projects('Education')
|
|
||||||
import_notification()
|
|
@ -7,7 +7,6 @@ from frappe import _
|
|||||||
|
|
||||||
from .operations import company_setup
|
from .operations import company_setup
|
||||||
from .operations import install_fixtures as fixtures
|
from .operations import install_fixtures as fixtures
|
||||||
from .operations import sample_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_setup_stages(args=None):
|
def get_setup_stages(args=None):
|
||||||
@ -103,16 +102,6 @@ def fin(args):
|
|||||||
frappe.local.message_log = []
|
frappe.local.message_log = []
|
||||||
login_as_first_user(args)
|
login_as_first_user(args)
|
||||||
|
|
||||||
make_sample_data(args.get('domains'))
|
|
||||||
|
|
||||||
def make_sample_data(domains):
|
|
||||||
try:
|
|
||||||
sample_data.make_sample_data(domains)
|
|
||||||
except Exception:
|
|
||||||
# clear message
|
|
||||||
if frappe.message_log:
|
|
||||||
frappe.message_log.pop()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def login_as_first_user(args):
|
def login_as_first_user(args):
|
||||||
if args.get("email") and hasattr(frappe.local, "login_manager"):
|
if args.get("email") and hasattr(frappe.local, "login_manager"):
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
Lets start making things in ERPNext that are representative of your institution.
|
|
||||||
|
|
||||||
1. Make a list of **Programs** that you offer
|
|
||||||
1. Add a few **Courses** that your programs cover
|
|
||||||
1. Create **Academic Terms** and **Academic Years**
|
|
||||||
1. Start adding **Students**
|
|
||||||
1. Group your students into **Batches**
|
|
||||||
|
|
||||||
Watch this video to learn more about ERPNext Education: https://www.youtube.com/watch?v=f6foQOyGzdA
|
|
@ -1,7 +0,0 @@
|
|||||||
Thanks for checking this out! ❤️
|
|
||||||
|
|
||||||
If you are evaluating an ERP system for the first time, this is going to be quite a task! But don't worry, ERPNext is awesome.
|
|
||||||
|
|
||||||
First, get familiar with the surroundings. ERPNext covers a *lot of features*, go to the home page and click on the "Explore" icon.
|
|
||||||
|
|
||||||
All the best!
|
|
@ -1,18 +0,0 @@
|
|||||||
Ready to go live with ERPNext? 🏁🏁🏁
|
|
||||||
|
|
||||||
Here are the steps:
|
|
||||||
|
|
||||||
1. Sync up your **Chart of Accounts**
|
|
||||||
3. Add your opening stock using **Stock Reconciliation**
|
|
||||||
4. Add your open invoices (both sales and purchase)
|
|
||||||
3. Add your opening account balances by making a **Journal Entry**
|
|
||||||
|
|
||||||
If you need help for going live, sign up for an account at erpnext.com or find a partner to help you with this.
|
|
||||||
|
|
||||||
Or you can watch these videos 📺:
|
|
||||||
|
|
||||||
Setup your chart of accounts: https://www.youtube.com/watch?v=AcfMCT7wLLo
|
|
||||||
|
|
||||||
Add Open Stock: https://www.youtube.com/watch?v=nlHX0ZZ84Lw
|
|
||||||
|
|
||||||
Add Opening Balances: https://www.youtube.com/watch?v=nlHX0ZZ84Lw
|
|
@ -1,5 +0,0 @@
|
|||||||
Lets import some data! 💪💪
|
|
||||||
|
|
||||||
If you are already running a business, you most likely have your Items, Customers or Suppliers in some spreadsheet file somewhere, import it into ERPNext with the Data Import Tool.
|
|
||||||
|
|
||||||
Watch this video to get started: https://www.youtube.com/watch?v=Ta2Xx3QoK3E
|
|
@ -1,7 +0,0 @@
|
|||||||
Start building a model of your business in ERPNext by adding your Items and Customers.
|
|
||||||
|
|
||||||
These videos 📺 will help you get started:
|
|
||||||
|
|
||||||
Adding Customers and Suppliers: https://www.youtube.com/watch?v=zsrrVDk6VBs
|
|
||||||
|
|
||||||
Adding Items and Prices: https://www.youtube.com/watch?v=FcOsV-e8ymE
|
|
@ -1,10 +0,0 @@
|
|||||||
How to manage your purchasing in ERPNext 🛒🛒🛒:
|
|
||||||
|
|
||||||
1. Add a few **Suppliers**
|
|
||||||
2. Find out what you need by making **Material Requests**.
|
|
||||||
3. Now start placing orders via **Purchase Order**.
|
|
||||||
4. When your suppliers deliver, make **Purchase Receipts**
|
|
||||||
|
|
||||||
Now never run out of stock again! 😎
|
|
||||||
|
|
||||||
Watch this video 📺 to get an overview: https://www.youtube.com/watch?v=4TN9kPyfIqM
|
|
@ -1,8 +0,0 @@
|
|||||||
Start managing your sales with ERPNext 🔔🔔🔔:
|
|
||||||
|
|
||||||
1. Add potential business contacts as **Leads**
|
|
||||||
2. Udpate your deals in pipeline in **Opportunities**
|
|
||||||
3. Send proposals to your leads or customers with **Quotations**
|
|
||||||
4. Track confirmed orders with **Sales Orders**
|
|
||||||
|
|
||||||
Watch this video 📺 to get an overview: https://www.youtube.com/watch?v=o9XCSZHJfpA
|
|
@ -1,5 +0,0 @@
|
|||||||
Lets import some data! 💪💪
|
|
||||||
|
|
||||||
If you are already running a Institute, you most likely have your Students in some spreadsheet file somewhere. Import it into ERPNext with the Data Import Tool.
|
|
||||||
|
|
||||||
Watch this video to get started: https://www.youtube.com/watch?v=Ta2Xx3QoK3E
|
|
@ -1,28 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"attach_print": 0,
|
|
||||||
"condition": "doc.status in ('Open', 'Overdue')",
|
|
||||||
"date_changed": "exp_end_date",
|
|
||||||
"days_in_advance": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Notification",
|
|
||||||
"document_type": "Task",
|
|
||||||
"enabled": 1,
|
|
||||||
"event": "Days After",
|
|
||||||
"is_standard": 0,
|
|
||||||
"message": "<p>Task due today:</p>\n\n<div>\n{{ doc.description }}\n</div>\n\n<hr>\n<p style=\"font-size: 85%\">\nThis is a notification for a task that is due today, and a sample <b>Notification</b>. In ERPNext you can setup notifications on anything, Invoices, Orders, Leads, Opportunities, so you never miss a thing.\n<br>To edit this, and setup other alerts, just type <b>Notification</b> in the search bar.</p>",
|
|
||||||
"method": null,
|
|
||||||
"modified": "2017-03-09 07:34:58.168370",
|
|
||||||
"module": null,
|
|
||||||
"name": "Task Due Alert",
|
|
||||||
"recipients": [
|
|
||||||
{
|
|
||||||
"cc": null,
|
|
||||||
"condition": null,
|
|
||||||
"email_by_document_field": "owner"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"subject": "{{ doc.subject }}",
|
|
||||||
"value_changed": null
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,12 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
|
||||||
|
|
||||||
|
|
||||||
def complete():
|
|
||||||
with open(os.path.join(os.path.dirname(__file__),
|
|
||||||
'data', 'test_mfg.json'), 'r') as f:
|
|
||||||
data = json.loads(f.read())
|
|
||||||
|
|
||||||
setup_complete(data)
|
|
@ -5,28 +5,17 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import add_days, flt, get_datetime_str, nowdate
|
from frappe.utils import add_days, flt, get_datetime_str, nowdate
|
||||||
|
from frappe.utils.data import now_datetime
|
||||||
|
from frappe.utils.nestedset import get_ancestors_of, get_root_of # noqa
|
||||||
|
|
||||||
from erpnext import get_default_company
|
from erpnext import get_default_company
|
||||||
|
|
||||||
|
|
||||||
def get_root_of(doctype):
|
|
||||||
"""Get root element of a DocType with a tree structure"""
|
|
||||||
result = frappe.db.sql_list("""select name from `tab%s`
|
|
||||||
where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" %
|
|
||||||
(doctype, doctype))
|
|
||||||
return result[0] if result else None
|
|
||||||
|
|
||||||
def get_ancestors_of(doctype, name):
|
|
||||||
"""Get ancestor elements of a DocType with a tree structure"""
|
|
||||||
lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"])
|
|
||||||
result = frappe.db.sql_list("""select name from `tab%s`
|
|
||||||
where lft<%s and rgt>%s order by lft desc""" % (doctype, "%s", "%s"), (lft, rgt))
|
|
||||||
return result or []
|
|
||||||
|
|
||||||
def before_tests():
|
def before_tests():
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
# complete setup if missing
|
# complete setup if missing
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
||||||
|
current_year = now_datetime().year
|
||||||
if not frappe.get_list("Company"):
|
if not frappe.get_list("Company"):
|
||||||
setup_complete({
|
setup_complete({
|
||||||
"currency" :"USD",
|
"currency" :"USD",
|
||||||
@ -36,8 +25,8 @@ def before_tests():
|
|||||||
"company_abbr" :"WP",
|
"company_abbr" :"WP",
|
||||||
"industry" :"Manufacturing",
|
"industry" :"Manufacturing",
|
||||||
"country" :"United States",
|
"country" :"United States",
|
||||||
"fy_start_date" :"2021-01-01",
|
"fy_start_date" :f"{current_year}-01-01",
|
||||||
"fy_end_date" :"2021-12-31",
|
"fy_end_date" :f"{current_year}-12-31",
|
||||||
"language" :"english",
|
"language" :"english",
|
||||||
"company_tagline" :"Testing",
|
"company_tagline" :"Testing",
|
||||||
"email" :"test@erpnext.com",
|
"email" :"test@erpnext.com",
|
||||||
@ -51,7 +40,6 @@ def before_tests():
|
|||||||
frappe.db.sql("delete from `tabSalary Slip`")
|
frappe.db.sql("delete from `tabSalary Slip`")
|
||||||
frappe.db.sql("delete from `tabItem Price`")
|
frappe.db.sql("delete from `tabItem Price`")
|
||||||
|
|
||||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
|
||||||
enable_all_roles_and_domains()
|
enable_all_roles_and_domains()
|
||||||
set_defaults_for_tests()
|
set_defaults_for_tests()
|
||||||
|
|
||||||
@ -142,13 +130,13 @@ def enable_all_roles_and_domains():
|
|||||||
add_all_roles_to('Administrator')
|
add_all_roles_to('Administrator')
|
||||||
|
|
||||||
def set_defaults_for_tests():
|
def set_defaults_for_tests():
|
||||||
from frappe.utils.nestedset import get_root_of
|
|
||||||
|
|
||||||
selling_settings = frappe.get_single("Selling Settings")
|
selling_settings = frappe.get_single("Selling Settings")
|
||||||
selling_settings.customer_group = get_root_of("Customer Group")
|
selling_settings.customer_group = get_root_of("Customer Group")
|
||||||
selling_settings.territory = get_root_of("Territory")
|
selling_settings.territory = get_root_of("Territory")
|
||||||
selling_settings.save()
|
selling_settings.save()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
|
|
||||||
def insert_record(records):
|
def insert_record(records):
|
||||||
for r in records:
|
for r in records:
|
||||||
|
@ -519,6 +519,8 @@ def make_sales_invoice(source_name, target_doc=None):
|
|||||||
if automatically_fetch_payment_terms:
|
if automatically_fetch_payment_terms:
|
||||||
doc.set_payment_schedule()
|
doc.set_payment_schedule()
|
||||||
|
|
||||||
|
doc.set_onload('ignore_price_list', True)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
@ -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",
|
||||||
|
@ -146,10 +146,6 @@ frappe.ui.form.on('Pick List', {
|
|||||||
customer: frm.doc.customer
|
customer: frm.doc.customer
|
||||||
};
|
};
|
||||||
frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
|
frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
|
||||||
if (!frm.doc.customer) {
|
|
||||||
frappe.msgprint(__('Please select Customer first'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
erpnext.utils.map_current_doc({
|
erpnext.utils.map_current_doc({
|
||||||
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
|
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
|
||||||
source_doctype: 'Sales Order',
|
source_doctype: 'Sales Order',
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict, defaultdict
|
from collections import OrderedDict, defaultdict
|
||||||
|
from itertools import groupby
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@ -24,8 +26,21 @@ class PickList(Document):
|
|||||||
def before_save(self):
|
def before_save(self):
|
||||||
self.set_item_locations()
|
self.set_item_locations()
|
||||||
|
|
||||||
|
# set percentage picked in SO
|
||||||
|
for location in self.get('locations'):
|
||||||
|
if location.sales_order and frappe.db.get_value("Sales Order",location.sales_order,"per_picked") == 100:
|
||||||
|
frappe.throw("Row " + str(location.idx) + " has been picked already!")
|
||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
for item in self.locations:
|
for item in self.locations:
|
||||||
|
# if the user has not entered any picked qty, set it to stock_qty, before submit
|
||||||
|
if item.picked_qty == 0:
|
||||||
|
item.picked_qty = item.stock_qty
|
||||||
|
|
||||||
|
if item.sales_order_item:
|
||||||
|
# update the picked_qty in SO Item
|
||||||
|
self.update_so(item.sales_order_item,item.picked_qty,item.item_code)
|
||||||
|
|
||||||
if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
|
if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
|
||||||
continue
|
continue
|
||||||
if not item.serial_no:
|
if not item.serial_no:
|
||||||
@ -37,6 +52,32 @@ class PickList(Document):
|
|||||||
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
|
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
|
||||||
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
|
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
|
||||||
|
|
||||||
|
def before_cancel(self):
|
||||||
|
#update picked_qty in SO Item on cancel of PL
|
||||||
|
for location in self.get('locations'):
|
||||||
|
if location.sales_order_item:
|
||||||
|
self.update_so(location.sales_order_item,0,location.item_code)
|
||||||
|
|
||||||
|
def update_so(self,so_item,picked_qty,item_code):
|
||||||
|
so_doc = frappe.get_doc("Sales Order",frappe.db.get_value("Sales Order Item",so_item,"parent"))
|
||||||
|
already_picked,actual_qty = frappe.db.get_value("Sales Order Item",so_item,["picked_qty","qty"])
|
||||||
|
|
||||||
|
if self.docstatus == 1:
|
||||||
|
if (((already_picked + picked_qty)/ actual_qty)*100) > (100 + flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance'))):
|
||||||
|
frappe.throw('You are picking more than required quantity for ' + item_code + '. Check if there is any other pick list created for '+so_doc.name)
|
||||||
|
|
||||||
|
frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty)
|
||||||
|
|
||||||
|
total_picked_qty = 0
|
||||||
|
total_so_qty = 0
|
||||||
|
for item in so_doc.get('items'):
|
||||||
|
total_picked_qty += flt(item.picked_qty)
|
||||||
|
total_so_qty += flt(item.stock_qty)
|
||||||
|
total_picked_qty=total_picked_qty + picked_qty
|
||||||
|
per_picked = total_picked_qty/total_so_qty * 100
|
||||||
|
|
||||||
|
so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_item_locations(self, save=False):
|
def set_item_locations(self, save=False):
|
||||||
self.validate_for_qty()
|
self.validate_for_qty()
|
||||||
@ -64,10 +105,6 @@ class PickList(Document):
|
|||||||
item_doc.name = None
|
item_doc.name = None
|
||||||
|
|
||||||
for row in locations:
|
for row in locations:
|
||||||
row.update({
|
|
||||||
'picked_qty': row.stock_qty
|
|
||||||
})
|
|
||||||
|
|
||||||
location = item_doc.as_dict()
|
location = item_doc.as_dict()
|
||||||
location.update(row)
|
location.update(row)
|
||||||
self.append('locations', location)
|
self.append('locations', location)
|
||||||
@ -340,63 +377,102 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ
|
|||||||
def create_delivery_note(source_name, target_doc=None):
|
def create_delivery_note(source_name, target_doc=None):
|
||||||
pick_list = frappe.get_doc('Pick List', source_name)
|
pick_list = frappe.get_doc('Pick List', source_name)
|
||||||
validate_item_locations(pick_list)
|
validate_item_locations(pick_list)
|
||||||
|
sales_dict = dict()
|
||||||
sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
|
sales_orders = []
|
||||||
sales_orders = set(sales_orders)
|
|
||||||
|
|
||||||
delivery_note = None
|
delivery_note = None
|
||||||
for sales_order in sales_orders:
|
for location in pick_list.locations:
|
||||||
delivery_note = create_delivery_note_from_sales_order(sales_order,
|
if location.sales_order:
|
||||||
delivery_note, skip_item_mapping=True)
|
sales_orders.append([frappe.db.get_value("Sales Order",location.sales_order,'customer'),location.sales_order])
|
||||||
|
# Group sales orders by customer
|
||||||
|
for key,keydata in groupby(sales_orders,key=itemgetter(0)):
|
||||||
|
sales_dict[key] = set([d[1] for d in keydata])
|
||||||
|
|
||||||
# map rows without sales orders as well
|
if sales_dict:
|
||||||
if not delivery_note:
|
delivery_note = create_dn_with_so(sales_dict,pick_list)
|
||||||
|
|
||||||
|
is_item_wo_so = 0
|
||||||
|
for location in pick_list.locations :
|
||||||
|
if not location.sales_order:
|
||||||
|
is_item_wo_so = 1
|
||||||
|
break
|
||||||
|
if is_item_wo_so == 1:
|
||||||
|
# Create a DN for items without sales orders as well
|
||||||
|
delivery_note = create_dn_wo_so(pick_list)
|
||||||
|
|
||||||
|
frappe.msgprint(_('Delivery Note(s) created for the Pick List'))
|
||||||
|
return delivery_note
|
||||||
|
|
||||||
|
def create_dn_wo_so(pick_list):
|
||||||
delivery_note = frappe.new_doc("Delivery Note")
|
delivery_note = frappe.new_doc("Delivery Note")
|
||||||
|
|
||||||
item_table_mapper = {
|
item_table_mapper_without_so = {
|
||||||
'doctype': 'Delivery Note Item',
|
'doctype': 'Delivery Note Item',
|
||||||
'field_map': {
|
'field_map': {
|
||||||
'rate': 'rate',
|
'rate': 'rate',
|
||||||
'name': 'so_detail',
|
'name': 'name',
|
||||||
'parent': 'against_sales_order',
|
'parent': '',
|
||||||
},
|
}
|
||||||
'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
|
|
||||||
}
|
|
||||||
|
|
||||||
item_table_mapper_without_so = {
|
|
||||||
'doctype': 'Delivery Note Item',
|
|
||||||
'field_map': {
|
|
||||||
'rate': 'rate',
|
|
||||||
'name': 'name',
|
|
||||||
'parent': '',
|
|
||||||
}
|
}
|
||||||
}
|
map_pl_locations(pick_list,item_table_mapper_without_so,delivery_note)
|
||||||
|
delivery_note.insert(ignore_mandatory = True)
|
||||||
|
|
||||||
|
return delivery_note
|
||||||
|
|
||||||
|
|
||||||
|
def create_dn_with_so(sales_dict,pick_list):
|
||||||
|
delivery_note = None
|
||||||
|
|
||||||
|
for customer in sales_dict:
|
||||||
|
for so in sales_dict[customer]:
|
||||||
|
delivery_note = None
|
||||||
|
delivery_note = create_delivery_note_from_sales_order(so,
|
||||||
|
delivery_note, skip_item_mapping=True)
|
||||||
|
|
||||||
|
item_table_mapper = {
|
||||||
|
'doctype': 'Delivery Note Item',
|
||||||
|
'field_map': {
|
||||||
|
'rate': 'rate',
|
||||||
|
'name': 'so_detail',
|
||||||
|
'parent': 'against_sales_order',
|
||||||
|
},
|
||||||
|
'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
|
||||||
|
}
|
||||||
|
break
|
||||||
|
if delivery_note:
|
||||||
|
# map all items of all sales orders of that customer
|
||||||
|
for so in sales_dict[customer]:
|
||||||
|
map_pl_locations(pick_list,item_table_mapper,delivery_note,so)
|
||||||
|
delivery_note.insert(ignore_mandatory = True)
|
||||||
|
|
||||||
|
return delivery_note
|
||||||
|
|
||||||
|
def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None):
|
||||||
|
|
||||||
for location in pick_list.locations:
|
for location in pick_list.locations:
|
||||||
if location.sales_order_item:
|
if location.sales_order == sales_order:
|
||||||
sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
|
if location.sales_order_item:
|
||||||
else:
|
sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
|
||||||
sales_order_item = None
|
else:
|
||||||
|
sales_order_item = None
|
||||||
|
|
||||||
source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
|
source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \
|
||||||
else [location, item_table_mapper_without_so]
|
else [location, item_mapper]
|
||||||
|
|
||||||
dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
|
dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
|
||||||
|
|
||||||
if dn_item:
|
if dn_item:
|
||||||
dn_item.warehouse = location.warehouse
|
dn_item.warehouse = location.warehouse
|
||||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||||
dn_item.batch_no = location.batch_no
|
dn_item.batch_no = location.batch_no
|
||||||
dn_item.serial_no = location.serial_no
|
dn_item.serial_no = location.serial_no
|
||||||
|
|
||||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
|
||||||
|
|
||||||
|
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||||
set_delivery_note_missing_values(delivery_note)
|
set_delivery_note_missing_values(delivery_note)
|
||||||
|
|
||||||
delivery_note.pick_list = pick_list.name
|
delivery_note.pick_list = pick_list.name
|
||||||
delivery_note.customer = pick_list.customer if pick_list.customer else None
|
delivery_note.company = pick_list.company
|
||||||
|
delivery_note.customer = frappe.get_value("Sales Order",sales_order,"customer")
|
||||||
|
|
||||||
return delivery_note
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_stock_entry(pick_list):
|
def create_stock_entry(pick_list):
|
||||||
|
@ -17,7 +17,6 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
|||||||
|
|
||||||
|
|
||||||
class TestPickList(FrappeTestCase):
|
class TestPickList(FrappeTestCase):
|
||||||
|
|
||||||
def test_pick_list_picks_warehouse_for_each_item(self):
|
def test_pick_list_picks_warehouse_for_each_item(self):
|
||||||
try:
|
try:
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
@ -188,7 +187,6 @@ class TestPickList(FrappeTestCase):
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
pick_list.set_item_locations()
|
pick_list.set_item_locations()
|
||||||
|
|
||||||
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
|
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
|
||||||
|
|
||||||
pr1.cancel()
|
pr1.cancel()
|
||||||
@ -311,6 +309,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
'item_code': '_Test Item',
|
'item_code': '_Test Item',
|
||||||
'qty': 1,
|
'qty': 1,
|
||||||
'conversion_factor': 5,
|
'conversion_factor': 5,
|
||||||
|
'stock_qty':5,
|
||||||
'delivery_date': frappe.utils.today()
|
'delivery_date': frappe.utils.today()
|
||||||
}, {
|
}, {
|
||||||
'item_code': '_Test Item',
|
'item_code': '_Test Item',
|
||||||
@ -329,9 +328,9 @@ class TestPickList(FrappeTestCase):
|
|||||||
'purpose': 'Delivery',
|
'purpose': 'Delivery',
|
||||||
'locations': [{
|
'locations': [{
|
||||||
'item_code': '_Test Item',
|
'item_code': '_Test Item',
|
||||||
'qty': 1,
|
'qty': 2,
|
||||||
'stock_qty': 5,
|
'stock_qty': 1,
|
||||||
'conversion_factor': 5,
|
'conversion_factor': 0.5,
|
||||||
'sales_order': sales_order.name,
|
'sales_order': sales_order.name,
|
||||||
'sales_order_item': sales_order.items[0].name ,
|
'sales_order_item': sales_order.items[0].name ,
|
||||||
}, {
|
}, {
|
||||||
@ -389,6 +388,95 @@ class TestPickList(FrappeTestCase):
|
|||||||
for expected_item, created_item in zip(expected_items, pl.locations):
|
for expected_item, created_item in zip(expected_items, pl.locations):
|
||||||
_compare_dicts(expected_item, created_item)
|
_compare_dicts(expected_item, created_item)
|
||||||
|
|
||||||
|
def test_multiple_dn_creation(self):
|
||||||
|
sales_order_1 = frappe.get_doc({
|
||||||
|
'doctype': 'Sales Order',
|
||||||
|
'customer': '_Test Customer',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'items': [{
|
||||||
|
'item_code': '_Test Item',
|
||||||
|
'qty': 1,
|
||||||
|
'conversion_factor': 1,
|
||||||
|
'delivery_date': frappe.utils.today()
|
||||||
|
}],
|
||||||
|
}).insert()
|
||||||
|
sales_order_1.submit()
|
||||||
|
sales_order_2 = frappe.get_doc({
|
||||||
|
'doctype': 'Sales Order',
|
||||||
|
'customer': '_Test Customer 1',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'items': [{
|
||||||
|
'item_code': '_Test Item 2',
|
||||||
|
'qty': 1,
|
||||||
|
'conversion_factor': 1,
|
||||||
|
'delivery_date': frappe.utils.today()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).insert()
|
||||||
|
sales_order_2.submit()
|
||||||
|
pick_list = frappe.get_doc({
|
||||||
|
'doctype': 'Pick List',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'items_based_on': 'Sales Order',
|
||||||
|
'purpose': 'Delivery',
|
||||||
|
'picker':'P001',
|
||||||
|
'locations': [{
|
||||||
|
'item_code': '_Test Item ',
|
||||||
|
'qty': 1,
|
||||||
|
'stock_qty': 1,
|
||||||
|
'conversion_factor': 1,
|
||||||
|
'sales_order': sales_order_1.name,
|
||||||
|
'sales_order_item': sales_order_1.items[0].name ,
|
||||||
|
}, {
|
||||||
|
'item_code': '_Test Item 2',
|
||||||
|
'qty': 1,
|
||||||
|
'stock_qty': 1,
|
||||||
|
'conversion_factor': 1,
|
||||||
|
'sales_order': sales_order_2.name,
|
||||||
|
'sales_order_item': sales_order_2.items[0].name ,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
pick_list.set_item_locations()
|
||||||
|
pick_list.submit()
|
||||||
|
create_delivery_note(pick_list.name)
|
||||||
|
for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer"},fields={"name"}):
|
||||||
|
for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
|
||||||
|
self.assertEqual(dn_item.item_code, '_Test Item')
|
||||||
|
self.assertEqual(dn_item.against_sales_order,sales_order_1.name)
|
||||||
|
for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer 1"},fields={"name"}):
|
||||||
|
for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
|
||||||
|
self.assertEqual(dn_item.item_code, '_Test Item 2')
|
||||||
|
self.assertEqual(dn_item.against_sales_order,sales_order_2.name)
|
||||||
|
#test DN creation without so
|
||||||
|
pick_list_1 = frappe.get_doc({
|
||||||
|
'doctype': 'Pick List',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'purpose': 'Delivery',
|
||||||
|
'picker':'P001',
|
||||||
|
'locations': [{
|
||||||
|
'item_code': '_Test Item ',
|
||||||
|
'qty': 1,
|
||||||
|
'stock_qty': 1,
|
||||||
|
'conversion_factor': 1,
|
||||||
|
}, {
|
||||||
|
'item_code': '_Test Item 2',
|
||||||
|
'qty': 2,
|
||||||
|
'stock_qty': 2,
|
||||||
|
'conversion_factor': 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
pick_list_1.set_item_locations()
|
||||||
|
pick_list_1.submit()
|
||||||
|
create_delivery_note(pick_list_1.name)
|
||||||
|
for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list_1.name},fields={"name"}):
|
||||||
|
for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
|
||||||
|
if dn_item.item_code == '_Test Item':
|
||||||
|
self.assertEqual(dn_item.qty,1)
|
||||||
|
if dn_item.item_code == '_Test Item 2':
|
||||||
|
self.assertEqual(dn_item.qty,2)
|
||||||
|
|
||||||
# def test_pick_list_skips_items_in_expired_batch(self):
|
# def test_pick_list_skips_items_in_expired_batch(self):
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
|
@ -823,6 +823,7 @@ def make_purchase_invoice(source_name, target_doc=None):
|
|||||||
}
|
}
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
|
doclist.set_onload('ignore_price_list', True)
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
def get_invoiced_qty_map(purchase_receipt):
|
def get_invoiced_qty_map(purchase_receipt):
|
||||||
|
@ -3,11 +3,22 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
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 +575,41 @@ 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(
|
||||||
filters = { "item_code": item_code, "warehouse": warehouse }
|
qty: float,
|
||||||
|
item_code: str,
|
||||||
|
warehouse: str,
|
||||||
|
posting_date: Optional[str] = None,
|
||||||
|
batch_nos: Optional[Union[str, List[str]]] = None,
|
||||||
|
for_doctype: Optional[str] = None,
|
||||||
|
exclude_sr_nos: Optional[List[str]] = None
|
||||||
|
) -> List[str]:
|
||||||
|
|
||||||
|
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
|
||||||
|
|
||||||
|
if exclude_sr_nos is None:
|
||||||
|
exclude_sr_nos = []
|
||||||
|
else:
|
||||||
|
exclude_sr_nos = safe_json_loads(exclude_sr_nos)
|
||||||
|
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]
|
else:
|
||||||
|
filters.batch_no = [str(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])
|
||||||
|
|
||||||
def get_delivered_serial_nos(serial_nos):
|
def get_delivered_serial_nos(serial_nos):
|
||||||
'''
|
'''
|
||||||
@ -650,37 +676,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,57 @@ 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=json.dumps(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 = []
|
||||||
|
@ -12,7 +12,7 @@ from frappe.utils import cint, date_diff, flt
|
|||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
Filters = frappe._dict
|
Filters = frappe._dict
|
||||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
|
||||||
|
|
||||||
def execute(filters: Filters = None) -> Tuple:
|
def execute(filters: Filters = None) -> Tuple:
|
||||||
to_date = filters["to_date"]
|
to_date = filters["to_date"]
|
||||||
@ -30,6 +30,8 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
|
|||||||
_func = itemgetter(1)
|
_func = itemgetter(1)
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
|
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||||
|
|
||||||
for item, item_dict in item_details.items():
|
for item, item_dict in item_details.items():
|
||||||
earliest_age, latest_age = 0, 0
|
earliest_age, latest_age = 0, 0
|
||||||
details = item_dict["details"]
|
details = item_dict["details"]
|
||||||
@ -76,6 +78,9 @@ def get_average_age(fifo_queue: List, to_date: str) -> float:
|
|||||||
return flt(age_qty / total_qty, 2) if total_qty else 0.0
|
return flt(age_qty / total_qty, 2) if total_qty else 0.0
|
||||||
|
|
||||||
def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
|
def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
|
||||||
|
|
||||||
|
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||||
|
|
||||||
range1 = range2 = range3 = above_range3 = 0.0
|
range1 = range2 = range3 = above_range3 = 0.0
|
||||||
|
|
||||||
for item in fifo_queue:
|
for item in fifo_queue:
|
||||||
|
@ -52,24 +52,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
frappe.ready(() => {
|
|
||||||
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
|
|
||||||
const $input = $(e.target);
|
|
||||||
const keyword = ($input.val() || '').toLowerCase();
|
|
||||||
const $filter_options = $input.next('.filter-options');
|
|
||||||
|
|
||||||
$filter_options.find('.custom-control').show();
|
|
||||||
$filter_options.find('.custom-control').each((i, el) => {
|
|
||||||
const $el = $(el);
|
|
||||||
const value = $el.data('value').toLowerCase();
|
|
||||||
if (!value.includes(keyword)) {
|
|
||||||
$el.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 300));
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -300,13 +300,13 @@
|
|||||||
|
|
||||||
{% if values | len > 20 %}
|
{% if values | len > 20 %}
|
||||||
<!-- show inline filter if values more than 20 -->
|
<!-- show inline filter if values more than 20 -->
|
||||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ item_field.label + 's' }}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if values %}
|
{% if values %}
|
||||||
<div class="filter-options">
|
<div class="filter-options">
|
||||||
{% for value in values %}
|
{% for value in values %}
|
||||||
<div class="checkbox" data-value="{{ value }}">
|
<div class="filter-lookup-wrapper checkbox" data-value="{{ value }}">
|
||||||
<label for="{{value}}">
|
<label for="{{value}}">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
class="product-filter field-filter"
|
class="product-filter field-filter"
|
||||||
@ -329,16 +329,16 @@
|
|||||||
{%- macro attribute_filter_section(filters)-%}
|
{%- macro attribute_filter_section(filters)-%}
|
||||||
{% for attribute in filters %}
|
{% for attribute in filters %}
|
||||||
<div class="mb-4 filter-block pb-5">
|
<div class="mb-4 filter-block pb-5">
|
||||||
<div class="filter-label mb-3">{{ attribute.name}}</div>
|
<div class="filter-label mb-3">{{ attribute.name }}</div>
|
||||||
{% if values | len > 20 %}
|
{% if attribute.item_attribute_values | len > 20 %}
|
||||||
<!-- show inline filter if values more than 20 -->
|
<!-- show inline filter if values more than 20 -->
|
||||||
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
|
<input type="text" class="form-control form-control-sm mb-2 filter-lookup-input" placeholder="Search {{ attribute.name + 's' }}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if attribute.item_attribute_values %}
|
{% if attribute.item_attribute_values %}
|
||||||
<div class="filter-options">
|
<div class="filter-options">
|
||||||
{% for attr_value in attribute.item_attribute_values %}
|
{% for attr_value in attribute.item_attribute_values %}
|
||||||
<div class="checkbox">
|
<div class="filter-lookup-wrapper checkbox" data-value="{{ attr_value }}">
|
||||||
<label data-value="{{ attr_value }}">
|
<label data-value="{{ attr_value }}">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
class="product-filter attribute-filter"
|
class="product-filter attribute-filter"
|
||||||
|
@ -31,24 +31,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
frappe.ready(() => {
|
|
||||||
$('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
|
|
||||||
const $input = $(e.target);
|
|
||||||
const keyword = ($input.val() || '').toLowerCase();
|
|
||||||
const $filter_options = $input.next('.filter-options');
|
|
||||||
|
|
||||||
$filter_options.find('.custom-control').show();
|
|
||||||
$filter_options.find('.custom-control').each((i, el) => {
|
|
||||||
const $el = $(el);
|
|
||||||
const value = $el.data('value').toLowerCase();
|
|
||||||
if (!value.includes(keyword)) {
|
|
||||||
$el.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 300));
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user