mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 04:15:10 +00:00
Merge pull request #30469 from frappe/version-13-pre-release
chore: v13.24.0 release
This commit is contained in:
@@ -33,6 +33,8 @@ class GLEntry(Document):
|
||||
name will be changed using autoname options (in a scheduled job)
|
||||
"""
|
||||
self.name = frappe.generate_hash(txt="", length=10)
|
||||
if self.meta.autoname == "hash":
|
||||
self.to_rename = 0
|
||||
|
||||
def validate(self):
|
||||
self.flags.ignore_submit_comment = True
|
||||
@@ -135,7 +137,7 @@ class GLEntry(Document):
|
||||
|
||||
def check_pl_account(self):
|
||||
if self.is_opening=='Yes' and \
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and not self.is_cancelled:
|
||||
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
|
||||
|
||||
@@ -376,8 +376,8 @@ def make_payment_request(**args):
|
||||
if args.order_type == "Shopping Cart" or args.mute_email:
|
||||
pr.flags.mute_email = True
|
||||
|
||||
pr.insert(ignore_permissions=True)
|
||||
if args.submit_doc:
|
||||
pr.insert(ignore_permissions=True)
|
||||
pr.submit()
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
@@ -394,7 +394,10 @@ def get_amount(ref_doc, payment_account=None):
|
||||
"""get amount based on doctype"""
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
|
||||
else:
|
||||
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) / ref_doc.conversion_rate
|
||||
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
|
||||
@@ -128,6 +128,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
pr1 = make_payment_request(dt="Sales Order", dn=so.name,
|
||||
recipient_id="nabin@erpnext.com", return_doc=1)
|
||||
pr1.grand_total = 200
|
||||
pr1.insert()
|
||||
pr1.submit()
|
||||
|
||||
# Make a 2nd Payment Request
|
||||
|
||||
@@ -456,6 +456,7 @@ class POSInvoice(SalesInvoice):
|
||||
pay_req = self.get_existing_payment_request(pay)
|
||||
if not pay_req:
|
||||
pay_req = self.get_new_payment_request(pay)
|
||||
pay_req.insert()
|
||||
pay_req.submit()
|
||||
else:
|
||||
pay_req.request_phone_payment()
|
||||
|
||||
@@ -812,12 +812,37 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
|
||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
|
||||
|
||||
pos.change_amount = 5.0
|
||||
pos.write_off_outstanding_amount_automatically = 1
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
self.assertEqual(pos.grand_total, 100.0)
|
||||
self.assertEqual(pos.write_off_amount, -5)
|
||||
self.assertEqual(pos.write_off_amount, 0)
|
||||
|
||||
def test_auto_write_off_amount(self):
|
||||
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
|
||||
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
|
||||
|
||||
make_purchase_receipt(company= "_Test Company with perpetual inventory",
|
||||
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
|
||||
|
||||
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
|
||||
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
|
||||
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
|
||||
cost_center = "Main - TCP1", do_not_save=True)
|
||||
|
||||
pos.is_pos = 1
|
||||
pos.update_stock = 1
|
||||
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
|
||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 40})
|
||||
|
||||
pos.write_off_outstanding_amount_automatically = 1
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
self.assertEqual(pos.grand_total, 100.0)
|
||||
self.assertEqual(pos.write_off_amount, 10)
|
||||
|
||||
def test_pos_with_no_gl_entry_for_change_amount(self):
|
||||
frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_to_date
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_to_date, flt, get_date_str
|
||||
|
||||
from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list
|
||||
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
|
||||
@@ -28,15 +29,22 @@ def get_mappers_from_db():
|
||||
|
||||
|
||||
def get_accounts_in_mappers(mapping_names):
|
||||
return frappe.db.sql('''
|
||||
select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability,
|
||||
cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment
|
||||
from `tabCash Flow Mapping Accounts` cfma
|
||||
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
|
||||
where cfma.parent in (%s)
|
||||
order by cfm.is_working_capital
|
||||
''', (', '.join('"%s"' % d for d in mapping_names)))
|
||||
cfm = frappe.qb.DocType('Cash Flow Mapping')
|
||||
cfma = frappe.qb.DocType('Cash Flow Mapping Accounts')
|
||||
result = (
|
||||
frappe.qb
|
||||
.select(
|
||||
cfma.name, cfm.label, cfm.is_working_capital,
|
||||
cfm.is_income_tax_liability, cfm.is_income_tax_expense,
|
||||
cfm.is_finance_cost, cfm.is_finance_cost_adjustment, cfma.account
|
||||
)
|
||||
.from_(cfm)
|
||||
.join(cfma)
|
||||
.on(cfm.name == cfma.parent)
|
||||
.where(cfma.parent.isin(mapping_names))
|
||||
).run()
|
||||
|
||||
return result
|
||||
|
||||
def setup_mappers(mappers):
|
||||
cash_flow_accounts = []
|
||||
@@ -57,31 +65,31 @@ def setup_mappers(mappers):
|
||||
|
||||
account_types = [
|
||||
dict(
|
||||
name=account[0], label=account[1], is_working_capital=account[2],
|
||||
name=account[0], account_name=account[7], label=account[1], is_working_capital=account[2],
|
||||
is_income_tax_liability=account[3], is_income_tax_expense=account[4]
|
||||
) for account in accounts if not account[3]]
|
||||
|
||||
finance_costs_adjustments = [
|
||||
dict(
|
||||
name=account[0], label=account[1], is_finance_cost=account[5],
|
||||
name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5],
|
||||
is_finance_cost_adjustment=account[6]
|
||||
) for account in accounts if account[6]]
|
||||
|
||||
tax_liabilities = [
|
||||
dict(
|
||||
name=account[0], label=account[1], is_income_tax_liability=account[3],
|
||||
name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
|
||||
is_income_tax_expense=account[4]
|
||||
) for account in accounts if account[3]]
|
||||
|
||||
tax_expenses = [
|
||||
dict(
|
||||
name=account[0], label=account[1], is_income_tax_liability=account[3],
|
||||
name=account[0], account_name=account[7], label=account[1], is_income_tax_liability=account[3],
|
||||
is_income_tax_expense=account[4]
|
||||
) for account in accounts if account[4]]
|
||||
|
||||
finance_costs = [
|
||||
dict(
|
||||
name=account[0], label=account[1], is_finance_cost=account[5])
|
||||
name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5])
|
||||
for account in accounts if account[5]]
|
||||
|
||||
account_types_labels = sorted(
|
||||
@@ -124,27 +132,27 @@ def setup_mappers(mappers):
|
||||
)
|
||||
|
||||
for label in account_types_labels:
|
||||
names = [d['name'] for d in account_types if d['label'] == label[0]]
|
||||
names = [d['account_name'] for d in account_types if d['label'] == label[0]]
|
||||
m = dict(label=label[0], names=names, is_working_capital=label[1])
|
||||
mapping['account_types'].append(m)
|
||||
|
||||
for label in fc_adjustment_labels:
|
||||
names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]]
|
||||
names = [d['account_name'] for d in finance_costs_adjustments if d['label'] == label[0]]
|
||||
m = dict(label=label[0], names=names)
|
||||
mapping['finance_costs_adjustments'].append(m)
|
||||
|
||||
for label in unique_liability_labels:
|
||||
names = [d['name'] for d in tax_liabilities if d['label'] == label[0]]
|
||||
names = [d['account_name'] for d in tax_liabilities if d['label'] == label[0]]
|
||||
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
|
||||
mapping['tax_liabilities'].append(m)
|
||||
|
||||
for label in unique_expense_labels:
|
||||
names = [d['name'] for d in tax_expenses if d['label'] == label[0]]
|
||||
names = [d['account_name'] for d in tax_expenses if d['label'] == label[0]]
|
||||
m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2])
|
||||
mapping['tax_expenses'].append(m)
|
||||
|
||||
for label in unique_finance_costs_labels:
|
||||
names = [d['name'] for d in finance_costs if d['label'] == label[0]]
|
||||
names = [d['account_name'] for d in finance_costs if d['label'] == label[0]]
|
||||
m = dict(label=label[0], names=names, is_finance_cost=label[1])
|
||||
mapping['finance_costs'].append(m)
|
||||
|
||||
@@ -371,14 +379,30 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0):
|
||||
if not account_names or not account_names[0] or not type(account_names[0]) == str:
|
||||
# only proceed if account_names is a list of account names
|
||||
return {}
|
||||
|
||||
from erpnext.accounts.report.cash_flow.cash_flow import get_start_date
|
||||
|
||||
company = filters.company
|
||||
data = {}
|
||||
total = 0
|
||||
GLEntry = frappe.qb.DocType('GL Entry')
|
||||
Account = frappe.qb.DocType('Account')
|
||||
|
||||
for period in period_list:
|
||||
start_date = get_start_date(period, accumulated_values, company)
|
||||
accounts = ', '.join('"%s"' % d for d in account_names)
|
||||
|
||||
account_subquery = (
|
||||
frappe.qb.from_(Account)
|
||||
.where(
|
||||
(Account.name.isin(account_names)) |
|
||||
(Account.parent_account.isin(account_names))
|
||||
)
|
||||
.select(Account.name)
|
||||
.as_("account_subquery")
|
||||
)
|
||||
|
||||
if opening_balances:
|
||||
date_info = dict(date=start_date)
|
||||
@@ -395,32 +419,31 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
|
||||
else:
|
||||
start, end = add_to_date(**date_info), add_to_date(**date_info)
|
||||
|
||||
gl_sum = frappe.db.sql_list("""
|
||||
select sum(credit) - sum(debit)
|
||||
from `tabGL Entry`
|
||||
where company=%s and posting_date >= %s and posting_date <= %s
|
||||
and voucher_type != 'Period Closing Voucher'
|
||||
and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
|
||||
OR parent_account IN (%s))
|
||||
""", (company, start, end, accounts, accounts))
|
||||
else:
|
||||
gl_sum = frappe.db.sql_list("""
|
||||
select sum(credit) - sum(debit)
|
||||
from `tabGL Entry`
|
||||
where company=%s and posting_date >= %s and posting_date <= %s
|
||||
and voucher_type != 'Period Closing Voucher'
|
||||
and account in ( SELECT name FROM tabAccount WHERE name IN (%s)
|
||||
OR parent_account IN (%s))
|
||||
""", (company, start_date if accumulated_values else period['from_date'],
|
||||
period['to_date'], accounts, accounts))
|
||||
start, end = get_date_str(start), get_date_str(end)
|
||||
|
||||
if gl_sum and gl_sum[0]:
|
||||
amount = gl_sum[0]
|
||||
else:
|
||||
amount = 0
|
||||
start, end = start_date if accumulated_values else period['from_date'], period['to_date']
|
||||
start, end = get_date_str(start), get_date_str(end)
|
||||
|
||||
total += amount
|
||||
data.setdefault(period["key"], amount)
|
||||
result = (
|
||||
frappe.qb.from_(GLEntry)
|
||||
.select(Sum(GLEntry.credit) - Sum(GLEntry.debit))
|
||||
.where(
|
||||
(GLEntry.company == company) &
|
||||
(GLEntry.posting_date >= start) &
|
||||
(GLEntry.posting_date <= end) &
|
||||
(GLEntry.voucher_type != 'Period Closing Voucher') &
|
||||
(GLEntry.account.isin(account_subquery))
|
||||
)
|
||||
).run()
|
||||
|
||||
if result and result[0]:
|
||||
gl_sum = result[0][0]
|
||||
else:
|
||||
gl_sum = 0
|
||||
|
||||
total += flt(gl_sum)
|
||||
data.setdefault(period["key"], flt(gl_sum))
|
||||
|
||||
data["total"] = total
|
||||
return data
|
||||
|
||||
@@ -168,7 +168,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
{account_type_condition}
|
||||
AND is_group = 0
|
||||
AND company = %(company)s
|
||||
AND account_currency = %(currency)s
|
||||
AND (account_currency = %(currency)s or ifnull(account_currency, '') = '')
|
||||
AND `{searchfield}` LIKE %(txt)s
|
||||
{mcond}
|
||||
ORDER BY idx DESC, name
|
||||
|
||||
@@ -580,7 +580,11 @@ class calculate_taxes_and_totals(object):
|
||||
.format(self.doc.party_account_currency, invoice_total))
|
||||
|
||||
if self.doc.docstatus == 0:
|
||||
if self.doc.get('write_off_outstanding_amount_automatically'):
|
||||
self.doc.write_off_amount = 0
|
||||
|
||||
self.calculate_outstanding_amount()
|
||||
self.calculate_write_off_amount()
|
||||
|
||||
def is_internal_invoice(self):
|
||||
"""
|
||||
@@ -621,7 +625,6 @@ class calculate_taxes_and_totals(object):
|
||||
change_amount = 0
|
||||
|
||||
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
|
||||
self.calculate_write_off_amount()
|
||||
self.calculate_change_amount()
|
||||
change_amount = self.doc.change_amount \
|
||||
if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount
|
||||
@@ -671,19 +674,20 @@ class calculate_taxes_and_totals(object):
|
||||
and self.doc.paid_amount > grand_total and not self.doc.is_return \
|
||||
and any(d.type == "Cash" for d in self.doc.payments):
|
||||
|
||||
self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
|
||||
self.doc.write_off_amount, self.doc.precision("change_amount"))
|
||||
self.doc.change_amount = flt(self.doc.paid_amount - grand_total,
|
||||
self.doc.precision("change_amount"))
|
||||
|
||||
self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total +
|
||||
self.doc.base_write_off_amount, self.doc.precision("base_change_amount"))
|
||||
self.doc.base_change_amount = flt(self.doc.base_paid_amount - base_grand_total,
|
||||
self.doc.precision("base_change_amount"))
|
||||
|
||||
def calculate_write_off_amount(self):
|
||||
if flt(self.doc.change_amount) > 0:
|
||||
self.doc.write_off_amount = flt(self.doc.grand_total - self.doc.paid_amount
|
||||
+ self.doc.change_amount, self.doc.precision("write_off_amount"))
|
||||
if self.doc.get('write_off_outstanding_amount_automatically'):
|
||||
self.doc.write_off_amount = flt(self.doc.outstanding_amount, self.doc.precision("write_off_amount"))
|
||||
self.doc.base_write_off_amount = flt(self.doc.write_off_amount * self.doc.conversion_rate,
|
||||
self.doc.precision("base_write_off_amount"))
|
||||
|
||||
self.calculate_outstanding_amount()
|
||||
|
||||
def calculate_margin(self, item):
|
||||
rate_with_margin = 0.0
|
||||
base_rate_with_margin = 0.0
|
||||
|
||||
@@ -5,6 +5,12 @@ frappe.ui.form.on('Website Item', {
|
||||
onload: function(frm) {
|
||||
// should never check Private
|
||||
frm.fields_dict["website_image"].df.is_private = 0;
|
||||
|
||||
frm.set_query("website_warehouse", () => {
|
||||
return {
|
||||
filters: {"is_group": 0}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
image: function() {
|
||||
|
||||
@@ -424,6 +424,22 @@ erpnext.ProductView = class {
|
||||
|
||||
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() {
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
"expected_discharge",
|
||||
"references",
|
||||
"admission_encounter",
|
||||
"admission_practitioner",
|
||||
"primary_practitioner",
|
||||
"medical_department",
|
||||
"admission_ordered_for",
|
||||
"expected_length_of_stay",
|
||||
"admission_service_unit_type",
|
||||
"cb_admission",
|
||||
"primary_practitioner",
|
||||
"secondary_practitioner",
|
||||
"admission_instruction",
|
||||
"encounter_details_section",
|
||||
@@ -134,11 +133,11 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "primary_practitioner.department",
|
||||
"fieldname": "medical_department",
|
||||
"fieldtype": "Link",
|
||||
"label": "Medical Department",
|
||||
"options": "Medical Department",
|
||||
"set_only_once": 1
|
||||
"options": "Medical Department"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_practitioner",
|
||||
@@ -211,13 +210,6 @@
|
||||
"fieldname": "cb_admission",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "admission_practitioner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Healthcare Practitioner",
|
||||
"options": "Healthcare Practitioner",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "admission_encounter",
|
||||
"fieldtype": "Link",
|
||||
@@ -412,7 +404,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-09 22:49:07.419692",
|
||||
"modified": "2022-02-22 12:15:02.843426",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Inpatient Record",
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"label": "Patient Appointment",
|
||||
"no_copy": 1,
|
||||
"options": "Patient Appointment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -82,7 +81,6 @@
|
||||
"fieldtype": "Link",
|
||||
"in_filter": 1,
|
||||
"label": "Patient Encounter",
|
||||
"no_copy": 1,
|
||||
"options": "Patient Encounter",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -258,7 +256,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-20 12:30:07.515185",
|
||||
"modified": "2022-02-19 11:48:16.347334",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Vital Signs",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
@@ -13,6 +13,7 @@ from erpnext.hr.doctype.employee_advance.employee_advance import (
|
||||
create_return_through_additional_salary,
|
||||
make_bank_entry,
|
||||
)
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import get_advances
|
||||
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
@@ -118,3 +119,24 @@ def make_employee_advance(employee_name, args=None):
|
||||
doc.submit()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_advances_for_claim(claim, advance_name, amount=None):
|
||||
advances = get_advances(claim.employee, advance_name)
|
||||
|
||||
for entry in advances:
|
||||
if amount:
|
||||
allocated_amount = amount
|
||||
else:
|
||||
allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount)
|
||||
|
||||
claim.append("advances", {
|
||||
"employee_advance": entry.name,
|
||||
"posting_date": entry.posting_date,
|
||||
"advance_account": entry.advance_account,
|
||||
"advance_paid": entry.paid_amount,
|
||||
"unclaimed_amount": allocated_amount,
|
||||
"allocated_amount": allocated_amount
|
||||
})
|
||||
|
||||
return claim
|
||||
@@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
validate_active_employee(self.employee)
|
||||
self.validate_advances()
|
||||
set_employee_name(self)
|
||||
self.validate_sanctioned_amount()
|
||||
self.calculate_total_amount()
|
||||
set_employee_name(self)
|
||||
self.validate_advances()
|
||||
self.set_expense_account(validate=True)
|
||||
self.set_payable_account()
|
||||
self.set_cost_center()
|
||||
@@ -42,10 +42,18 @@ class ExpenseClaim(AccountsController):
|
||||
"2": "Cancelled"
|
||||
}[cstr(self.docstatus or 0)]
|
||||
|
||||
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
|
||||
precision = self.precision("grand_total")
|
||||
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
|
||||
and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
|
||||
|
||||
if (
|
||||
# set as paid
|
||||
self.is_paid
|
||||
or (flt(self.total_sanctioned_amount > 0) and (
|
||||
# grand total is reimbursed
|
||||
(self.docstatus == 1 and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision))
|
||||
# grand total (to be paid) is 0 since linked advances already cover the claimed amount
|
||||
or (flt(self.grand_total, precision) == 0)
|
||||
))
|
||||
) and self.approval_status == "Approved":
|
||||
status = "Paid"
|
||||
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
|
||||
status = "Unpaid"
|
||||
|
||||
@@ -72,6 +72,72 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
|
||||
self.assertEqual(expense_claim.status, "Unpaid")
|
||||
|
||||
# expense claim without any sanctioned amount should not have status as Paid
|
||||
claim = make_expense_claim(payable_account, 1000, 0, "_Test Company", "Travel Expenses - _TC")
|
||||
self.assertEqual(claim.total_sanctioned_amount, 0)
|
||||
self.assertEqual(claim.status, "Submitted")
|
||||
|
||||
# no gl entries created
|
||||
gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': claim.name})
|
||||
self.assertEqual(len(gl_entry), 0)
|
||||
|
||||
def test_expense_claim_against_fully_paid_advances(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
make_payment_entry,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||
|
||||
advance = make_employee_advance(claim.employee)
|
||||
pe = make_payment_entry(advance)
|
||||
pe.submit()
|
||||
|
||||
# claim for already paid out advances
|
||||
claim = get_advances_for_claim(claim, advance.name)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_partially_paid_via_advance(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
)
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
make_payment_entry as make_advance_payment,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True)
|
||||
|
||||
# link advance for partial amount
|
||||
advance = make_employee_advance(claim.employee, {'advance_amount': 500})
|
||||
pe = make_advance_payment(advance)
|
||||
pe.submit()
|
||||
|
||||
claim = get_advances_for_claim(claim, advance.name)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 500)
|
||||
self.assertEqual(claim.status, "Unpaid")
|
||||
|
||||
# reimburse remaning amount
|
||||
make_payment_entry(claim, payable_account, 500)
|
||||
claim.reload()
|
||||
|
||||
self.assertEqual(claim.total_amount_reimbursed, 500)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_gl_entry(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
|
||||
@@ -25,6 +25,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
|
||||
LeaveDayBlockedError,
|
||||
NotAnOptionalHoliday,
|
||||
OverlapError,
|
||||
get_leave_allocation_records,
|
||||
get_leave_balance_on,
|
||||
get_leave_details,
|
||||
)
|
||||
@@ -881,6 +882,27 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(leave_allocation['leaves_pending_approval'], 1)
|
||||
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):
|
||||
# initial leave allocation
|
||||
@@ -902,6 +924,8 @@ def create_carry_forwarded_allocation(employee, leave_type):
|
||||
carry_forward=1)
|
||||
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):
|
||||
allocation = frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
|
||||
@@ -8,14 +8,15 @@ from math import ceil
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import date_diff, flt, formatdate, get_last_day, getdate
|
||||
from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate
|
||||
from six import string_types
|
||||
|
||||
|
||||
class LeavePolicyAssignment(Document):
|
||||
def validate(self):
|
||||
self.validate_policy_assignment_overlap()
|
||||
self.set_dates()
|
||||
self.validate_policy_assignment_overlap()
|
||||
self.warn_about_carry_forwarding()
|
||||
|
||||
def on_submit(self):
|
||||
self.grant_leave_alloc_for_employee()
|
||||
@@ -39,6 +40,20 @@ class LeavePolicyAssignment(Document):
|
||||
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
|
||||
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
|
||||
|
||||
def warn_about_carry_forwarding(self):
|
||||
if not self.carry_forward:
|
||||
return
|
||||
|
||||
leave_types = get_leave_type_details()
|
||||
leave_policy = frappe.get_doc("Leave Policy", self.leave_policy)
|
||||
|
||||
for policy in leave_policy.leave_policy_details:
|
||||
leave_type = leave_types.get(policy.leave_type)
|
||||
if not leave_type.is_carry_forward:
|
||||
msg = _("Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled.").format(
|
||||
frappe.bold(get_link_to_form("Leave Type", leave_type.name)))
|
||||
frappe.msgprint(msg, indicator="orange", alert=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def grant_leave_alloc_for_employee(self):
|
||||
if self.leaves_allocated:
|
||||
|
||||
@@ -500,6 +500,9 @@ class JobCard(Document):
|
||||
2: "Cancelled"
|
||||
}[self.docstatus or 0]
|
||||
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
self.status = 'Material Transferred'
|
||||
|
||||
if self.time_logs:
|
||||
self.status = 'Work In Progress'
|
||||
|
||||
@@ -507,10 +510,6 @@ class JobCard(Document):
|
||||
(self.for_quantity <= self.total_completed_qty or not self.items)):
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status != 'Completed':
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
self.status = 'Material Transferred'
|
||||
|
||||
if update_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 = frappe.get_doc("Job Card", job_card_name)
|
||||
self.assertEqual(job_card.status, "Open")
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Production Plan', {
|
||||
|
||||
before_save: function(frm) {
|
||||
// preserve temporary names on production plan item to re-link sub-assembly items
|
||||
frm.doc.po_items.forEach(item => {
|
||||
item.temporary_name = item.name;
|
||||
});
|
||||
},
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
'Work Order': 'Work Order / Subcontract PO',
|
||||
|
||||
@@ -351,7 +351,6 @@
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "get_items_from",
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
@@ -379,7 +378,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-14 03:56:23.209247",
|
||||
"modified": "2022-03-25 09:15:25.017664",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -32,6 +32,7 @@ class ProductionPlan(Document):
|
||||
self.set_pending_qty_in_row_without_reference()
|
||||
self.calculate_total_planned_qty()
|
||||
self.set_status()
|
||||
self._rename_temporary_references()
|
||||
|
||||
def set_pending_qty_in_row_without_reference(self):
|
||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||
@@ -57,6 +58,18 @@ class ProductionPlan(Document):
|
||||
if not flt(d.planned_qty):
|
||||
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
|
||||
|
||||
def _rename_temporary_references(self):
|
||||
""" po_items and sub_assembly_items items are both constructed client side without saving.
|
||||
|
||||
Attempt to fix linkages by using temporary names to map final row names.
|
||||
"""
|
||||
new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
|
||||
actual_names = {d.name for d in self.po_items}
|
||||
|
||||
for sub_assy in self.sub_assembly_items:
|
||||
if sub_assy.production_plan_item not in actual_names:
|
||||
sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_sales_orders(self):
|
||||
""" Pull sales orders which are pending to deliver based on criteria selected"""
|
||||
@@ -982,21 +995,21 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
|
||||
required_qty = item.get("quantity")
|
||||
# get available material by transferring to production warehouse
|
||||
for d in locations:
|
||||
if required_qty <=0: return
|
||||
if required_qty <= 0:
|
||||
return
|
||||
|
||||
new_dict = copy.deepcopy(item)
|
||||
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
|
||||
|
||||
if required_qty > 0:
|
||||
new_dict.update({
|
||||
"quantity": quantity,
|
||||
"material_request_type": "Material Transfer",
|
||||
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
|
||||
"from_warehouse": d.get("warehouse")
|
||||
})
|
||||
new_dict.update({
|
||||
"quantity": quantity,
|
||||
"material_request_type": "Material Transfer",
|
||||
"uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM
|
||||
"from_warehouse": d.get("warehouse")
|
||||
})
|
||||
|
||||
required_qty -= quantity
|
||||
new_mr_items.append(new_dict)
|
||||
required_qty -= quantity
|
||||
new_mr_items.append(new_dict)
|
||||
|
||||
# raise purchase request for remaining qty
|
||||
if required_qty:
|
||||
|
||||
@@ -617,6 +617,39 @@ class TestProductionPlan(FrappeTestCase):
|
||||
wo_doc.submit()
|
||||
self.assertEqual(wo_doc.qty, 0.55)
|
||||
|
||||
def test_temporary_name_relinking(self):
|
||||
|
||||
pp = frappe.new_doc("Production Plan")
|
||||
|
||||
# this can not be unittested so mocking data that would be expected
|
||||
# from client side.
|
||||
for _ in range(10):
|
||||
po_item = pp.append("po_items", {
|
||||
"name": frappe.generate_hash(length=10),
|
||||
"temporary_name": frappe.generate_hash(length=10),
|
||||
})
|
||||
pp.append("sub_assembly_items", {
|
||||
"production_plan_item": po_item.temporary_name
|
||||
})
|
||||
pp._rename_temporary_references()
|
||||
|
||||
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
|
||||
self.assertEqual(po_item.name, subassy_item.production_plan_item)
|
||||
|
||||
# bad links should be erased
|
||||
pp.append("sub_assembly_items", {
|
||||
"production_plan_item": frappe.generate_hash(length=16)
|
||||
})
|
||||
pp._rename_temporary_references()
|
||||
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
|
||||
pp.sub_assembly_items.pop()
|
||||
|
||||
# reattempting on same doc shouldn't change anything
|
||||
pp._rename_temporary_references()
|
||||
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
|
||||
self.assertEqual(po_item.name, subassy_item.production_plan_item)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
sales_order (obj): Sales Order Doc Object
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"product_bundle_item",
|
||||
"item_reference"
|
||||
"item_reference",
|
||||
"temporary_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -204,17 +205,25 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Item Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "temporary_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "temporary name"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-28 18:31:06.822168",
|
||||
"modified": "2022-03-24 04:54:09.940224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -352,7 +352,6 @@ class TestWorkOrder(FrappeTestCase):
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(),
|
||||
sales_order=so.name, qty=3)
|
||||
|
||||
wo_order.submit()
|
||||
self.assertEqual(wo_order.docstatus, 1)
|
||||
|
||||
allow_overproduction("overproduction_percentage_for_sales_order", 0)
|
||||
|
||||
@@ -457,7 +457,8 @@ class WorkOrder(Document):
|
||||
mr_obj.update_requested_qty([self.material_request_item])
|
||||
|
||||
def update_ordered_qty(self):
|
||||
if self.production_plan and self.production_plan_item:
|
||||
if self.production_plan and self.production_plan_item \
|
||||
and not self.production_plan_sub_assembly_item:
|
||||
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
|
||||
|
||||
if self.docstatus == 1:
|
||||
@@ -640,9 +641,13 @@ class WorkOrder(Document):
|
||||
if not self.qty > 0:
|
||||
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
||||
|
||||
if self.production_plan and self.production_plan_item:
|
||||
if self.production_plan and self.production_plan_item \
|
||||
and not self.production_plan_sub_assembly_item:
|
||||
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
|
||||
|
||||
if not qty_dict:
|
||||
return
|
||||
|
||||
allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings",
|
||||
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
|
||||
|
||||
@@ -1146,6 +1151,10 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create
|
||||
doc.insert()
|
||||
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
|
||||
|
||||
def get_work_order_operation_data(work_order, operation, workstation):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
erpnext.patches.v12_0.update_is_cancelled_field
|
||||
erpnext.patches.v13_0.add_bin_unique_constraint
|
||||
erpnext.patches.v11_0.rename_production_order_to_work_order
|
||||
erpnext.patches.v13_0.add_bin_unique_constraint
|
||||
erpnext.patches.v11_0.refactor_naming_series
|
||||
erpnext.patches.v11_0.refactor_autoname_naming
|
||||
execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28
|
||||
@@ -352,5 +352,8 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo
|
||||
erpnext.patches.v13_0.amazon_mws_deprecation_warning
|
||||
erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
||||
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
|
||||
erpnext.patches.v13_0.rename_non_profit_fields
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
|
||||
erpnext.patches.v13_0.rename_non_profit_fields
|
||||
erpnext.patches.v13_0.enable_ksa_vat_docs #1
|
||||
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
|
||||
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
|
||||
if not company:
|
||||
return
|
||||
|
||||
sales_invoice_gst_fields = [
|
||||
dict(fieldname='billing_address_gstin', label='Billing Address GSTIN',
|
||||
fieldtype='Data', insert_after='customer_address', read_only=1,
|
||||
fetch_from='customer_address.gstin', print_hide=1, length=15),
|
||||
dict(fieldname='customer_gstin', label='Customer GSTIN',
|
||||
fieldtype='Data', insert_after='shipping_address_name',
|
||||
fetch_from='shipping_address_name.gstin', print_hide=1, length=15),
|
||||
dict(fieldname='place_of_supply', label='Place of Supply',
|
||||
fieldtype='Data', insert_after='customer_gstin',
|
||||
print_hide=1, read_only=1, length=50),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', insert_after='company_address',
|
||||
fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15),
|
||||
]
|
||||
|
||||
custom_fields = {
|
||||
'Quotation': sales_invoice_gst_fields
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields, update=True)
|
||||
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()
|
||||
@@ -60,7 +60,7 @@ def execute():
|
||||
|
||||
def convert_to_seconds(value, unit):
|
||||
seconds = 0
|
||||
if value == 0:
|
||||
if not value:
|
||||
return seconds
|
||||
if unit == 'Hours':
|
||||
seconds = value * 3600
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Update Expense Claim status to Paid if:
|
||||
- the entire required amount is already covered via linked advances
|
||||
- the claim is partially paid via advances and the rest is reimbursed
|
||||
"""
|
||||
|
||||
ExpenseClaim = frappe.qb.DocType('Expense Claim')
|
||||
|
||||
(frappe.qb
|
||||
.update(ExpenseClaim)
|
||||
.set(ExpenseClaim.status, 'Paid')
|
||||
.where(
|
||||
((ExpenseClaim.grand_total == 0) | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed))
|
||||
& (ExpenseClaim.approval_status == 'Approved')
|
||||
& (ExpenseClaim.docstatus == 1)
|
||||
& (ExpenseClaim.total_sanctioned_amount > 0)
|
||||
)
|
||||
).run()
|
||||
@@ -660,6 +660,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr
|
||||
if not_submitted_ss:
|
||||
frappe.msgprint(_("Could not submit some Salary Slips"))
|
||||
|
||||
frappe.flags.via_payroll_entry = False
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -781,11 +781,12 @@ class SalarySlip(TransactionBase):
|
||||
previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component)
|
||||
|
||||
# get taxable_earnings for current period (all days)
|
||||
current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption)
|
||||
current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption, payroll_period=payroll_period)
|
||||
future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1)
|
||||
|
||||
# get taxable_earnings, addition_earnings for current actual payment days
|
||||
current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1)
|
||||
current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption,
|
||||
based_on_payment_days=1, payroll_period=payroll_period)
|
||||
current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings
|
||||
current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income
|
||||
current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax
|
||||
@@ -911,7 +912,7 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
return total_tax_paid
|
||||
|
||||
def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0):
|
||||
def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None):
|
||||
joining_date, relieving_date = self.get_joining_and_relieving_dates()
|
||||
|
||||
taxable_earnings = 0
|
||||
@@ -938,7 +939,7 @@ class SalarySlip(TransactionBase):
|
||||
# Get additional amount based on future recurring additional salary
|
||||
if additional_amount and earning.is_recurring_additional_salary:
|
||||
additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
|
||||
earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month
|
||||
earning.additional_amount, payroll_period) # Used earning.additional_amount to consider the amount for the full month
|
||||
|
||||
if earning.deduct_full_tax_on_selected_payroll_date:
|
||||
additional_income_with_full_tax += additional_amount
|
||||
@@ -955,7 +956,7 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
if additional_amount and ded.is_recurring_additional_salary:
|
||||
additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
|
||||
ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month
|
||||
ded.additional_amount, payroll_period) # Used ded.additional_amount to consider the amount for the full month
|
||||
|
||||
return frappe._dict({
|
||||
"taxable_earnings": taxable_earnings,
|
||||
@@ -964,12 +965,18 @@ class SalarySlip(TransactionBase):
|
||||
"flexi_benefits": flexi_benefits
|
||||
})
|
||||
|
||||
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
|
||||
def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount, payroll_period):
|
||||
future_recurring_additional_amount = 0
|
||||
to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
|
||||
|
||||
# future month count excluding current
|
||||
from_date, to_date = getdate(self.start_date), getdate(to_date)
|
||||
|
||||
# If recurring period end date is beyond the payroll period,
|
||||
# last day of payroll period should be considered for recurring period calculation
|
||||
if getdate(to_date) > getdate(payroll_period.end_date):
|
||||
to_date = getdate(payroll_period.end_date)
|
||||
|
||||
future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month)
|
||||
|
||||
if future_recurring_period > 0:
|
||||
|
||||
@@ -38,6 +38,8 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar
|
||||
class TestSalarySlip(unittest.TestCase):
|
||||
def setUp(self):
|
||||
setup_test()
|
||||
frappe.flags.pop("via_payroll_entry", None)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
@@ -413,15 +415,17 @@ class TestSalarySlip(unittest.TestCase):
|
||||
"email_salary_slip_to_employee": 1
|
||||
})
|
||||
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")
|
||||
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
||||
user_id = "test_email_salary_slip@salary.com"
|
||||
|
||||
make_employee(user_id, company="_Test Company")
|
||||
ss = make_employee_salary_slip(user_id, "Monthly", "Test Salary Slip Email")
|
||||
ss.company = "_Test Company"
|
||||
ss.save()
|
||||
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)
|
||||
|
||||
def test_loan_repayment_salary_slip(self):
|
||||
|
||||
@@ -34,12 +34,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
|
||||
},
|
||||
|
||||
calculate_taxes_and_totals: function(update_paid_amount) {
|
||||
calculate_taxes_and_totals: async function(update_paid_amount) {
|
||||
this.discount_amount_applied = false;
|
||||
this._calculate_taxes_and_totals();
|
||||
this.calculate_discount_amount();
|
||||
|
||||
this.calculate_shipping_charges();
|
||||
await this.calculate_shipping_charges();
|
||||
|
||||
// Advance calculation applicable to Sales /Purchase Invoice
|
||||
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
|
||||
@@ -273,28 +273,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
calculate_shipping_charges: function() {
|
||||
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
|
||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
|
||||
this.shipping_rule();
|
||||
this._calculate_taxes_and_totals();
|
||||
}
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template: function(item_tax_map) {
|
||||
let me = this;
|
||||
|
||||
if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
|
||||
if (typeof (item_tax_map) == "string") {
|
||||
item_tax_map = JSON.parse(item_tax_map);
|
||||
}
|
||||
|
||||
$.each(item_tax_map, function(tax, rate) {
|
||||
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
|
||||
if (!found) {
|
||||
let child = frappe.model.add_child(me.frm.doc, "taxes");
|
||||
child.charge_type = "On Net Total";
|
||||
child.account_head = tax;
|
||||
child.rate = 0;
|
||||
}
|
||||
});
|
||||
return this.shipping_rule();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -687,7 +666,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
}));
|
||||
this.frm.doc.total_advance = flt(total_allocated_amount, precision("total_advance"));
|
||||
|
||||
if (this.frm.doc.write_off_outstanding_amount_automatically) {
|
||||
this.frm.doc.write_off_amount = 0;
|
||||
}
|
||||
|
||||
this.calculate_outstanding_amount(update_paid_amount);
|
||||
this.calculate_write_off_amount();
|
||||
},
|
||||
|
||||
is_internal_invoice: function() {
|
||||
@@ -812,7 +796,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
this.frm.set_value('base_paid_amount', flt(base_paid_amount, precision("base_paid_amount")));
|
||||
},
|
||||
|
||||
calculate_change_amount: function(){
|
||||
calculate_change_amount: function() {
|
||||
this.frm.doc.change_amount = 0.0;
|
||||
this.frm.doc.base_change_amount = 0.0;
|
||||
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
|
||||
@@ -823,26 +807,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||
var base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
|
||||
|
||||
this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total +
|
||||
this.frm.doc.write_off_amount, precision("change_amount"));
|
||||
this.frm.doc.change_amount = flt(this.frm.doc.paid_amount - grand_total,
|
||||
precision("change_amount"));
|
||||
|
||||
this.frm.doc.base_change_amount = flt(this.frm.doc.base_paid_amount -
|
||||
base_grand_total + this.frm.doc.base_write_off_amount,
|
||||
precision("base_change_amount"));
|
||||
base_grand_total, precision("base_change_amount"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
calculate_write_off_amount: function(){
|
||||
if(this.frm.doc.paid_amount > this.frm.doc.grand_total){
|
||||
this.frm.doc.write_off_amount = flt(this.frm.doc.grand_total - this.frm.doc.paid_amount
|
||||
+ this.frm.doc.change_amount, precision("write_off_amount"));
|
||||
|
||||
calculate_write_off_amount: function() {
|
||||
if (this.frm.doc.write_off_outstanding_amount_automatically) {
|
||||
this.frm.doc.write_off_amount = flt(this.frm.doc.outstanding_amount, precision("write_off_amount"));
|
||||
this.frm.doc.base_write_off_amount = flt(this.frm.doc.write_off_amount * this.frm.doc.conversion_rate,
|
||||
precision("base_write_off_amount"));
|
||||
}else{
|
||||
this.frm.doc.paid_amount = 0.0;
|
||||
|
||||
this.calculate_outstanding_amount(false);
|
||||
}
|
||||
this.calculate_outstanding_amount(false);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -736,6 +736,26 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
});
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template: function(item_tax_map) {
|
||||
let me = this;
|
||||
|
||||
if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
|
||||
if (typeof (item_tax_map) == "string") {
|
||||
item_tax_map = JSON.parse(item_tax_map);
|
||||
}
|
||||
|
||||
$.each(item_tax_map, function(tax, rate) {
|
||||
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
|
||||
if (!found) {
|
||||
let child = frappe.model.add_child(me.frm.doc, "taxes");
|
||||
child.charge_type = "On Net Total";
|
||||
child.account_head = tax;
|
||||
child.rate = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
serial_no: function(doc, cdt, cdn) {
|
||||
var me = this;
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
@@ -1066,6 +1086,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "apply_shipping_rule",
|
||||
callback: function(r) {
|
||||
me._calculate_taxes_and_totals();
|
||||
}
|
||||
}).fail(() => this.frm.set_value('shipping_rule', ''));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -75,7 +75,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
fieldtype:'Float',
|
||||
read_only: me.has_batch && !me.has_serial_no,
|
||||
label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
|
||||
default: flt(me.item.stock_qty),
|
||||
default: flt(me.item.stock_qty) || flt(me.item.transfer_qty),
|
||||
},
|
||||
...get_pending_qty_fields(me),
|
||||
{
|
||||
@@ -94,14 +94,16 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
description: __('Fetch Serial Numbers based on FIFO'),
|
||||
click: () => {
|
||||
let qty = this.dialog.fields_dict.qty.get_value();
|
||||
let already_selected_serial_nos = get_selected_serial_nos(me);
|
||||
let numbers = frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
|
||||
args: {
|
||||
qty: qty,
|
||||
item_code: me.item_code,
|
||||
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
|
||||
batch_no: me.item.batch_no || null,
|
||||
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date
|
||||
batch_nos: me.item.batch_no || null,
|
||||
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
|
||||
exclude_sr_nos: already_selected_serial_nos
|
||||
}
|
||||
});
|
||||
|
||||
@@ -575,21 +577,40 @@ function get_pending_qty_fields(me) {
|
||||
return pending_qty_fields;
|
||||
}
|
||||
|
||||
function calc_total_selected_qty(me) {
|
||||
// get all items with same item code except row for which selector is open.
|
||||
function get_rows_with_same_item_code(me) {
|
||||
const { frm: { doc: { items }}, item: { name, item_code }} = me;
|
||||
const totalSelectedQty = items
|
||||
.filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
|
||||
.map( item => flt(item.qty) )
|
||||
.reduce( (i, j) => i + j, 0);
|
||||
return items.filter(item => (item.name !== name) && (item.item_code === item_code))
|
||||
}
|
||||
|
||||
function calc_total_selected_qty(me) {
|
||||
const totalSelectedQty = get_rows_with_same_item_code(me)
|
||||
.map(item => flt(item.qty))
|
||||
.reduce((i, j) => i + j, 0);
|
||||
return totalSelectedQty;
|
||||
}
|
||||
|
||||
function get_selected_serial_nos(me) {
|
||||
const selected_serial_nos = get_rows_with_same_item_code(me)
|
||||
.map(item => item.serial_no)
|
||||
.filter(serial => serial)
|
||||
.map(sr_no_string => sr_no_string.split('\n'))
|
||||
.reduce((acc, arr) => acc.concat(arr), [])
|
||||
.filter(serial => serial);
|
||||
return selected_serial_nos;
|
||||
};
|
||||
|
||||
function check_can_calculate_pending_qty(me) {
|
||||
const { frm: { doc }, item } = me;
|
||||
const docChecks = doc.bom_no
|
||||
&& doc.fg_completed_qty
|
||||
&& erpnext.stock.bom
|
||||
&& 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;
|
||||
}
|
||||
|
||||
//# sourceURL=serial_no_batch_selector.js
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
--green-info: #38A160;
|
||||
--product-bg-color: white;
|
||||
--body-bg-color: var(--gray-50);
|
||||
--text-md: 13px; // variables are in desk folder in frappe for v13, this is a temporary fix
|
||||
}
|
||||
|
||||
body.product-page {
|
||||
@@ -264,6 +265,15 @@ body.product-page {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-lookup-input {
|
||||
background-color: white;
|
||||
border: 1px solid var(--gray-300);
|
||||
|
||||
&:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -547,6 +547,7 @@ def get_custom_fields():
|
||||
'Journal Entry': journal_entry_fields,
|
||||
'Sales Order': sales_invoice_gst_fields,
|
||||
'Tax Category': inter_state_gst_field,
|
||||
'Quotation': sales_invoice_gst_fields,
|
||||
'Item': [
|
||||
dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
||||
fieldtype='Link', options='GST HSN Code', insert_after='item_group'),
|
||||
|
||||
@@ -21,8 +21,9 @@ PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
|
||||
|
||||
|
||||
def validate_gstin_for_india(doc, method):
|
||||
if hasattr(doc, 'gst_state') and doc.gst_state:
|
||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
||||
if hasattr(doc, 'gst_state'):
|
||||
set_gst_state_and_state_number(doc)
|
||||
|
||||
if not hasattr(doc, 'gstin') or not doc.gstin:
|
||||
return
|
||||
|
||||
@@ -52,7 +53,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"))
|
||||
|
||||
validate_gstin_check_digit(doc.gstin)
|
||||
set_gst_state_and_state_number(doc)
|
||||
|
||||
if not doc.gst_state:
|
||||
frappe.throw(_("Please enter GST state"), title=_("Invalid State"))
|
||||
@@ -84,17 +84,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')
|
||||
|
||||
def set_gst_state_and_state_number(doc):
|
||||
if not doc.gst_state:
|
||||
if not doc.state:
|
||||
return
|
||||
if not doc.gst_state and doc.state:
|
||||
state = doc.state.lower()
|
||||
states_lowercase = {s.lower():s for s in states}
|
||||
if state in states_lowercase:
|
||||
doc.gst_state = states_lowercase[state]
|
||||
else:
|
||||
return
|
||||
|
||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
||||
doc.gst_state_number = state_numbers.get(doc.gst_state)
|
||||
|
||||
def validate_gstin_check_digit(gstin, label='GSTIN'):
|
||||
''' Function to validate the check digit of the GSTIN.'''
|
||||
@@ -181,7 +178,7 @@ def test_method():
|
||||
def get_place_of_supply(party_details, doctype):
|
||||
if not frappe.get_meta('Address').has_field('gst_state'): return
|
||||
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"):
|
||||
address_name = party_details.customer_address or party_details.shipping_address_name
|
||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||
address_name = party_details.shipping_address or party_details.supplier_address
|
||||
@@ -207,7 +204,7 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
party_details.taxes = []
|
||||
return party_details
|
||||
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"):
|
||||
master_doctype = "Sales Taxes and Charges Template"
|
||||
tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
|
||||
|
||||
@@ -243,7 +240,7 @@ def update_party_details(party_details, doctype):
|
||||
party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field)))
|
||||
|
||||
def is_internal_transfer(party_details, doctype):
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"):
|
||||
destination_gstin = party_details.company_gstin
|
||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||
destination_gstin = party_details.supplier_gstin
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"col_break98",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"currency_and_price_list",
|
||||
@@ -116,7 +118,9 @@
|
||||
{
|
||||
"fieldname": "customer_section",
|
||||
"fieldtype": "Section Break",
|
||||
"options": "fa fa-user"
|
||||
"options": "fa fa-user",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -126,7 +130,9 @@
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
@@ -138,7 +144,9 @@
|
||||
"options": "SAL-QTN-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Customer",
|
||||
@@ -150,7 +158,9 @@
|
||||
"oldfieldtype": "Select",
|
||||
"options": "DocType",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
@@ -163,7 +173,9 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "quotation_to",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
@@ -172,12 +184,16 @@
|
||||
"hidden": 1,
|
||||
"in_global_search": 1,
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -191,6 +207,8 @@
|
||||
"options": "Quotation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
@@ -203,6 +221,8 @@
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
@@ -217,12 +237,16 @@
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "valid_till",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Till"
|
||||
"label": "Valid Till",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Sales",
|
||||
@@ -234,7 +258,9 @@
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nSales\nMaintenance\nShopping Cart",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -242,14 +268,18 @@
|
||||
"fieldname": "contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact",
|
||||
"options": "fa fa-bullhorn"
|
||||
"options": "fa fa-bullhorn",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "address_display",
|
||||
@@ -257,7 +287,9 @@
|
||||
"label": "Address",
|
||||
"oldfieldname": "customer_address",
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_person",
|
||||
@@ -266,20 +298,26 @@
|
||||
"oldfieldname": "contact_person",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_display",
|
||||
"fieldtype": "Small Text",
|
||||
"in_global_search": 1,
|
||||
"label": "Contact",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_email",
|
||||
@@ -288,12 +326,16 @@
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name",
|
||||
"fieldname": "col_break98",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -301,14 +343,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Shipping Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_address",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Shipping Address",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name",
|
||||
@@ -319,21 +365,27 @@
|
||||
"oldfieldname": "customer_group",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Customer Group",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"label": "Territory",
|
||||
"options": "Territory",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "currency_and_price_list",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Currency and Price List",
|
||||
"options": "fa fa-tag"
|
||||
"options": "fa fa-tag",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
@@ -344,6 +396,8 @@
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -356,11 +410,15 @@
|
||||
"precision": "9",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -372,6 +430,8 @@
|
||||
"options": "Price List",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
@@ -381,7 +441,9 @@
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "Rate at which Price list currency is converted to company's base currency",
|
||||
@@ -390,7 +452,9 @@
|
||||
"label": "Price List Exchange Rate",
|
||||
"precision": "9",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -399,13 +463,17 @@
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
"options": "fa fa-shopping-cart",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
@@ -416,29 +484,39 @@
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Quotation Item",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "40px"
|
||||
},
|
||||
{
|
||||
"fieldname": "pricing_rule_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Pricing Rules"
|
||||
"label": "Pricing Rules",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pricing_rules",
|
||||
"fieldtype": "Table",
|
||||
"label": "Pricing Rule Detail",
|
||||
"options": "Pricing Rule Detail",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sec_break23",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Quantity",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total",
|
||||
@@ -446,7 +524,9 @@
|
||||
"label": "Total (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_net_total",
|
||||
@@ -457,18 +537,24 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_28",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "net_total",
|
||||
@@ -476,32 +562,42 @@
|
||||
"label": "Net Total",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_net_weight",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Net Weight",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxes and Charges",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-money"
|
||||
"options": "fa fa-money",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Category",
|
||||
"options": "Tax Category",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_34",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shipping_rule",
|
||||
@@ -509,11 +605,15 @@
|
||||
"label": "Shipping Rule",
|
||||
"oldfieldtype": "Button",
|
||||
"options": "Shipping Rule",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_36",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes_and_charges",
|
||||
@@ -522,7 +622,9 @@
|
||||
"oldfieldname": "charge",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Sales Taxes and Charges Template",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes",
|
||||
@@ -530,13 +632,17 @@
|
||||
"label": "Sales Taxes and Charges",
|
||||
"oldfieldname": "other_charges",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Sales Taxes and Charges"
|
||||
"options": "Sales Taxes and Charges",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sec_tax_breakup",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Breakup"
|
||||
"label": "Tax Breakup",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "other_charges_calculation",
|
||||
@@ -545,11 +651,15 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldtype": "HTML",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_39",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total_taxes_and_charges",
|
||||
@@ -559,11 +669,15 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_42",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_taxes_and_charges",
|
||||
@@ -571,26 +685,34 @@
|
||||
"label": "Total Taxes and Charges",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "discount_amount",
|
||||
"fieldname": "section_break_44",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Discount and Coupon Code"
|
||||
"label": "Additional Discount and Coupon Code",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "coupon_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Coupon Code",
|
||||
"options": "Coupon Code"
|
||||
"options": "Coupon Code",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "referral_sales_partner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Referral Sales Partner",
|
||||
"options": "Sales Partner"
|
||||
"options": "Sales Partner",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "Grand Total",
|
||||
@@ -598,7 +720,9 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Apply Additional Discount On",
|
||||
"options": "\nGrand Total\nNet Total",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_discount_amount",
|
||||
@@ -606,31 +730,41 @@
|
||||
"label": "Additional Discount Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_46",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_discount_percentage",
|
||||
"fieldtype": "Float",
|
||||
"label": "Additional Discount Percentage",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional Discount Amount",
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "totals",
|
||||
"fieldtype": "Section Break",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-money",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_grand_total",
|
||||
@@ -641,6 +775,8 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
@@ -650,7 +786,9 @@
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"description": "In Words will be visible once you save the Quotation.",
|
||||
@@ -662,6 +800,8 @@
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
@@ -673,6 +813,8 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
@@ -680,6 +822,8 @@
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -691,6 +835,8 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
@@ -700,7 +846,9 @@
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
@@ -711,6 +859,8 @@
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
@@ -722,19 +872,25 @@
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "200px"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Terms"
|
||||
"label": "Payment Terms",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_terms_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms Template",
|
||||
"options": "Payment Terms Template",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_schedule",
|
||||
@@ -742,7 +898,9 @@
|
||||
"label": "Payment Schedule",
|
||||
"no_copy": 1,
|
||||
"options": "Payment Schedule",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -751,7 +909,9 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Terms and Conditions",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-legal"
|
||||
"options": "fa fa-legal",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tc_name",
|
||||
@@ -761,20 +921,26 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Terms and Conditions",
|
||||
"print_hide": 1,
|
||||
"report_hide": 1
|
||||
"report_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "terms",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Term Details",
|
||||
"oldfieldname": "terms",
|
||||
"oldfieldtype": "Text Editor"
|
||||
"oldfieldtype": "Text Editor",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "print_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
"label": "Print Settings",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -784,7 +950,9 @@
|
||||
"oldfieldname": "letter_head",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Letter Head",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -792,11 +960,15 @@
|
||||
"fieldname": "group_same_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Group same items",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_73",
|
||||
"fieldtype": "Column Break"
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -808,19 +980,25 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Print Heading",
|
||||
"print_hide": 1,
|
||||
"report_hide": 1
|
||||
"report_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Data",
|
||||
"label": "Print Language",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat Section"
|
||||
"label": "Auto Repeat Section",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "auto_repeat",
|
||||
@@ -829,14 +1007,18 @@
|
||||
"no_copy": 1,
|
||||
"options": "Auto Repeat",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval: doc.auto_repeat",
|
||||
"fieldname": "update_auto_repeat_reference",
|
||||
"fieldtype": "Button",
|
||||
"label": "Update Auto Repeat Reference"
|
||||
"label": "Update Auto Repeat Reference",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -845,7 +1027,9 @@
|
||||
"label": "More Information",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "campaign",
|
||||
@@ -854,7 +1038,9 @@
|
||||
"oldfieldname": "campaign",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Campaign",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "source",
|
||||
@@ -863,7 +1049,9 @@
|
||||
"oldfieldname": "source",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Lead Source",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -874,13 +1062,17 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "order_lost_reason",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break4",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -895,7 +1087,9 @@
|
||||
"options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "enq_det",
|
||||
@@ -905,13 +1099,17 @@
|
||||
"oldfieldname": "enq_det",
|
||||
"oldfieldtype": "Text",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_quotation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Quotation",
|
||||
"options": "Supplier Quotation"
|
||||
"options": "Supplier Quotation",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity",
|
||||
@@ -919,7 +1117,9 @@
|
||||
"label": "Opportunity",
|
||||
"options": "Opportunity",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -927,7 +1127,9 @@
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Lost Reasons",
|
||||
"options": "Quotation Lost Reason Detail",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "packed_items",
|
||||
@@ -935,7 +1137,9 @@
|
||||
"fieldtype": "Table",
|
||||
"label": "Bundle Items",
|
||||
"options": "Packed Item",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -945,14 +1149,32 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Bundle Items",
|
||||
"options": "fa fa-suitcase",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Address Name",
|
||||
"options": "Address",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_address_display",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Company Address",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-30 01:33:21.106073",
|
||||
"modified": "2022-03-23 16:49:36.297403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
3
erpnext/selling/doctype/quotation/regional/india.js
Normal file
3
erpnext/selling/doctype/quotation/regional/india.js
Normal file
@@ -0,0 +1,3 @@
|
||||
{% include "erpnext/regional/india/taxes.js" %}
|
||||
|
||||
erpnext.setup_auto_gst_taxation('Quotation');
|
||||
@@ -130,6 +130,7 @@
|
||||
"per_delivered",
|
||||
"column_break_81",
|
||||
"per_billed",
|
||||
"per_picked",
|
||||
"billing_status",
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
@@ -1514,13 +1515,19 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "per_picked",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Picked",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-05 12:16:40.775704",
|
||||
"modified": "2022-03-15 21:38:31.437586",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
@@ -1594,6 +1601,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"picked_qty",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -796,12 +797,17 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "picked_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Picked Qty"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-24 14:41:57.325799",
|
||||
"modified": "2022-03-15 20:17:33.984799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -108,6 +108,7 @@ class Item(Document):
|
||||
self.validate_variant_attributes()
|
||||
self.validate_variant_based_on_change()
|
||||
self.validate_fixed_asset()
|
||||
self.clear_retain_sample()
|
||||
self.validate_retain_sample()
|
||||
self.validate_uom_conversion_factor()
|
||||
self.validate_customer_provided_part()
|
||||
@@ -210,6 +211,13 @@ class Item(Document):
|
||||
frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format(
|
||||
self.item_code))
|
||||
|
||||
def clear_retain_sample(self):
|
||||
if not self.has_batch_no:
|
||||
self.retain_sample = None
|
||||
|
||||
if not self.retain_sample:
|
||||
self.sample_quantity = None
|
||||
|
||||
def add_default_uom_in_conversion_factor_table(self):
|
||||
if not self.is_new() and self.has_value_changed("stock_uom"):
|
||||
self.uoms = []
|
||||
|
||||
@@ -622,6 +622,20 @@ class TestItem(FrappeTestCase):
|
||||
item.item_group = "All Item Groups"
|
||||
item.save() # if item code saved without item_code then series worked
|
||||
|
||||
@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 set_item_variant_settings(fields):
|
||||
doc = frappe.get_doc('Item Variant Settings')
|
||||
|
||||
@@ -83,6 +83,9 @@ class MaterialRequest(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_schedule_date()
|
||||
|
||||
def validate_material_request_type(self):
|
||||
""" Validate fields in accordance with selected type """
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"bold": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "schedule_date",
|
||||
@@ -459,7 +460,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-03 14:40:24.409826",
|
||||
"modified": "2022-03-10 18:42:42.705190",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request Item",
|
||||
|
||||
@@ -146,10 +146,6 @@ frappe.ui.form.on('Pick List', {
|
||||
customer: frm.doc.customer
|
||||
};
|
||||
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({
|
||||
method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list',
|
||||
source_doctype: 'Sales Order',
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import json
|
||||
from collections import OrderedDict, defaultdict
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -24,8 +26,21 @@ class PickList(Document):
|
||||
def before_save(self):
|
||||
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):
|
||||
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'):
|
||||
continue
|
||||
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')
|
||||
.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 item in self.get('locations'):
|
||||
if item.sales_order_item:
|
||||
self.update_so(item.sales_order_item, -1 * item.picked_qty, item.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()
|
||||
def set_item_locations(self, save=False):
|
||||
self.validate_for_qty()
|
||||
@@ -64,10 +105,6 @@ class PickList(Document):
|
||||
item_doc.name = None
|
||||
|
||||
for row in locations:
|
||||
row.update({
|
||||
'picked_qty': row.stock_qty
|
||||
})
|
||||
|
||||
location = item_doc.as_dict()
|
||||
location.update(row)
|
||||
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):
|
||||
pick_list = frappe.get_doc('Pick List', source_name)
|
||||
validate_item_locations(pick_list)
|
||||
|
||||
sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
|
||||
sales_orders = set(sales_orders)
|
||||
|
||||
sales_dict = dict()
|
||||
sales_orders = []
|
||||
delivery_note = None
|
||||
for sales_order in sales_orders:
|
||||
delivery_note = create_delivery_note_from_sales_order(sales_order,
|
||||
delivery_note, skip_item_mapping=True)
|
||||
for location in pick_list.locations:
|
||||
if location.sales_order:
|
||||
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 not delivery_note:
|
||||
if sales_dict:
|
||||
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")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
item_table_mapper_without_so = {
|
||||
'doctype': 'Delivery Note Item',
|
||||
'field_map': {
|
||||
'rate': 'rate',
|
||||
'name': 'name',
|
||||
'parent': '',
|
||||
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:
|
||||
if location.sales_order_item:
|
||||
sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
|
||||
else:
|
||||
sales_order_item = None
|
||||
if location.sales_order == sales_order:
|
||||
if location.sales_order_item:
|
||||
sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
|
||||
else:
|
||||
sales_order_item = None
|
||||
|
||||
source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
|
||||
else [location, item_table_mapper_without_so]
|
||||
source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \
|
||||
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:
|
||||
dn_item.warehouse = location.warehouse
|
||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||
dn_item.batch_no = location.batch_no
|
||||
dn_item.serial_no = location.serial_no
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
if dn_item:
|
||||
dn_item.warehouse = location.warehouse
|
||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||
dn_item.batch_no = location.batch_no
|
||||
dn_item.serial_no = location.serial_no
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
set_delivery_note_missing_values(delivery_note)
|
||||
|
||||
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()
|
||||
def create_stock_entry(pick_list):
|
||||
|
||||
@@ -17,7 +17,6 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||
|
||||
|
||||
class TestPickList(FrappeTestCase):
|
||||
|
||||
def test_pick_list_picks_warehouse_for_each_item(self):
|
||||
try:
|
||||
frappe.get_doc({
|
||||
@@ -188,7 +187,6 @@ class TestPickList(FrappeTestCase):
|
||||
}]
|
||||
})
|
||||
pick_list.set_item_locations()
|
||||
|
||||
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
|
||||
|
||||
pr1.cancel()
|
||||
@@ -311,6 +309,7 @@ class TestPickList(FrappeTestCase):
|
||||
'item_code': '_Test Item',
|
||||
'qty': 1,
|
||||
'conversion_factor': 5,
|
||||
'stock_qty':5,
|
||||
'delivery_date': frappe.utils.today()
|
||||
}, {
|
||||
'item_code': '_Test Item',
|
||||
@@ -329,9 +328,9 @@ class TestPickList(FrappeTestCase):
|
||||
'purpose': 'Delivery',
|
||||
'locations': [{
|
||||
'item_code': '_Test Item',
|
||||
'qty': 1,
|
||||
'stock_qty': 5,
|
||||
'conversion_factor': 5,
|
||||
'qty': 2,
|
||||
'stock_qty': 1,
|
||||
'conversion_factor': 0.5,
|
||||
'sales_order': sales_order.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):
|
||||
_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):
|
||||
# pass
|
||||
|
||||
|
||||
@@ -3,13 +3,22 @@
|
||||
|
||||
|
||||
import json
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate
|
||||
from six import string_types
|
||||
from six.moves import map
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
safe_json_loads,
|
||||
)
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.get_item_details import get_reserved_qty_for_so
|
||||
@@ -583,30 +592,45 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
|
||||
return serial_nos
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch_nos=None, for_doctype=None):
|
||||
filters = { "item_code": item_code, "warehouse": warehouse }
|
||||
def auto_fetch_serial_number(
|
||||
qty: 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:
|
||||
try:
|
||||
filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
|
||||
except Exception:
|
||||
filters["batch_no"] = [batch_nos]
|
||||
batch_nos = safe_json_loads(batch_nos)
|
||||
if isinstance(batch_nos, list):
|
||||
filters.batch_no = batch_nos
|
||||
else:
|
||||
filters.batch_no = [str(batch_nos)]
|
||||
|
||||
if posting_date:
|
||||
filters["expiry_date"] = posting_date
|
||||
filters.expiry_date = posting_date
|
||||
|
||||
serial_numbers = []
|
||||
if for_doctype == 'POS Invoice':
|
||||
reserved_sr_nos = get_pos_reserved_serial_nos(filters)
|
||||
serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=reserved_sr_nos)
|
||||
else:
|
||||
serial_numbers = fetch_serial_numbers(filters, qty)
|
||||
exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters))
|
||||
|
||||
return [d.get('name') for d in serial_numbers]
|
||||
serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos)
|
||||
|
||||
return sorted([d.get('name') for d in serial_numbers])
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_reserved_serial_nos(filters):
|
||||
if isinstance(filters, string_types):
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no
|
||||
@@ -629,37 +653,37 @@ def get_pos_reserved_serial_nos(filters):
|
||||
def fetch_serial_numbers(filters, qty, do_not_include=None):
|
||||
if do_not_include is None:
|
||||
do_not_include = []
|
||||
batch_join_selection = ""
|
||||
batch_no_condition = ""
|
||||
|
||||
batch_nos = filters.get("batch_no")
|
||||
expiry_date = filters.get("expiry_date")
|
||||
serial_no = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
frappe.qb
|
||||
.from_(serial_no)
|
||||
.select(serial_no.name)
|
||||
.where(
|
||||
(serial_no.item_code == filters["item_code"])
|
||||
& (serial_no.warehouse == filters["warehouse"])
|
||||
& (Coalesce(serial_no.sales_invoice, "") == "")
|
||||
& (Coalesce(serial_no.delivery_document_no, "") == "")
|
||||
)
|
||||
.orderby(serial_no.creation)
|
||||
.limit(qty or 1)
|
||||
)
|
||||
|
||||
if do_not_include:
|
||||
query = query.where(serial_no.name.notin(do_not_include))
|
||||
|
||||
if batch_nos:
|
||||
batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos))
|
||||
query = query.where(serial_no.batch_no.isin(batch_nos))
|
||||
|
||||
if expiry_date:
|
||||
batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name "
|
||||
expiry_date_cond = "AND ifnull(batch.expiry_date, '2500-12-31') >= %(expiry_date)s "
|
||||
batch_no_condition += expiry_date_cond
|
||||
|
||||
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)
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
query = (query
|
||||
.left_join(batch).on(serial_no.batch_no == batch.name)
|
||||
.where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date)
|
||||
)
|
||||
|
||||
serial_numbers = query.run(as_dict=True)
|
||||
return serial_numbers
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.serial_no.serial_no import *
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
@@ -18,9 +20,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
test_dependencies = ["Item"]
|
||||
test_records = frappe.get_test_records('Serial No')
|
||||
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import *
|
||||
|
||||
|
||||
class TestSerialNo(FrappeTestCase):
|
||||
@@ -242,3 +241,57 @@ class TestSerialNo(FrappeTestCase):
|
||||
)
|
||||
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, [])
|
||||
|
||||
@@ -27,6 +27,8 @@ class StockLedgerEntry(Document):
|
||||
name will be changed using autoname options (in a scheduled job)
|
||||
"""
|
||||
self.name = frappe.generate_hash(txt="", length=10)
|
||||
if self.meta.autoname == "hash":
|
||||
self.to_rename = 0
|
||||
|
||||
def validate(self):
|
||||
self.flags.ignore_submit_comment = True
|
||||
|
||||
@@ -5,9 +5,11 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
@@ -529,3 +531,62 @@ def create_items():
|
||||
make_item(d, properties=properties)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
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
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cint
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children
|
||||
|
||||
test_records = frappe.get_test_records('Warehouse')
|
||||
|
||||
@@ -65,6 +65,33 @@ class TestWarehouse(FrappeTestCase):
|
||||
f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}."
|
||||
)
|
||||
|
||||
def test_group_non_group_conversion(self):
|
||||
|
||||
warehouse = frappe.get_doc("Warehouse", create_warehouse("TestGroupConversion"))
|
||||
|
||||
convert_to_group_or_ledger(warehouse.name)
|
||||
warehouse.reload()
|
||||
self.assertEqual(warehouse.is_group, 1)
|
||||
|
||||
child = create_warehouse("GroupWHChild", {"parent_warehouse": warehouse.name})
|
||||
# chid exists
|
||||
self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
|
||||
frappe.delete_doc("Warehouse", child)
|
||||
|
||||
convert_to_group_or_ledger(warehouse.name)
|
||||
warehouse.reload()
|
||||
self.assertEqual(warehouse.is_group, 0)
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target=warehouse.name, qty=1)
|
||||
# SLE exists
|
||||
self.assertRaises(frappe.ValidationError, convert_to_group_or_ledger, warehouse.name)
|
||||
|
||||
def test_get_children(self):
|
||||
company = "_Test Company"
|
||||
|
||||
children = get_children("Warehouse", parent=company, company=company, is_root=True)
|
||||
self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children))
|
||||
|
||||
|
||||
def create_warehouse(warehouse_name, properties=None, company=None):
|
||||
if not company:
|
||||
|
||||
@@ -41,14 +41,11 @@ class Warehouse(NestedSet):
|
||||
|
||||
def on_trash(self):
|
||||
# delete bin
|
||||
bins = frappe.db.sql("select * from `tabBin` where warehouse = %s",
|
||||
self.name, as_dict=1)
|
||||
bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name})
|
||||
for d in bins:
|
||||
if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \
|
||||
d['indented_qty'] or d['projected_qty'] or d['planned_qty']:
|
||||
throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code']))
|
||||
else:
|
||||
frappe.db.sql("delete from `tabBin` where name = %s", d['name'])
|
||||
|
||||
if self.check_if_sle_exists():
|
||||
throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse."))
|
||||
@@ -56,16 +53,15 @@ class Warehouse(NestedSet):
|
||||
if self.check_if_child_exists():
|
||||
throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
|
||||
|
||||
frappe.db.delete("Bin", filters={"warehouse": self.name})
|
||||
self.update_nsm_model()
|
||||
self.unlink_from_items()
|
||||
|
||||
def check_if_sle_exists(self):
|
||||
return frappe.db.sql("""select name from `tabStock Ledger Entry`
|
||||
where warehouse = %s limit 1""", self.name)
|
||||
return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
|
||||
|
||||
def check_if_child_exists(self):
|
||||
return frappe.db.sql("""select name from `tabWarehouse`
|
||||
where parent_warehouse = %s limit 1""", self.name)
|
||||
return frappe.db.exists("Warehouse", {"parent_warehouse": self.name})
|
||||
|
||||
def convert_to_group_or_ledger(self):
|
||||
if self.is_group:
|
||||
@@ -92,10 +88,7 @@ class Warehouse(NestedSet):
|
||||
return 1
|
||||
|
||||
def unlink_from_items(self):
|
||||
frappe.db.sql("""
|
||||
update `tabItem Default`
|
||||
set default_warehouse=NULL
|
||||
where default_warehouse=%s""", self.name)
|
||||
frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(doctype, parent=None, company=None, is_root=False):
|
||||
@@ -164,15 +157,16 @@ def add_node():
|
||||
frappe.get_doc(args).insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_to_group_or_ledger():
|
||||
args = frappe.form_dict
|
||||
return frappe.get_doc("Warehouse", args.docname).convert_to_group_or_ledger()
|
||||
def convert_to_group_or_ledger(docname=None):
|
||||
if not docname:
|
||||
docname = frappe.form_dict.docname
|
||||
return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger()
|
||||
|
||||
def get_child_warehouses(warehouse):
|
||||
lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
return frappe.db.sql_list("""select name from `tabWarehouse`
|
||||
where lft >= %s and rgt <= %s""", (lft, rgt))
|
||||
children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft")
|
||||
return children + [warehouse] # append self for backward compatibility
|
||||
|
||||
def get_warehouses_based_on_account(account, company=None):
|
||||
warehouses = []
|
||||
|
||||
@@ -52,24 +52,6 @@
|
||||
|
||||
</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>
|
||||
|
||||
@@ -300,13 +300,13 @@
|
||||
|
||||
{% if values | len > 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 %}
|
||||
|
||||
{% if values %}
|
||||
<div class="filter-options">
|
||||
{% for value in values %}
|
||||
<div class="checkbox" data-value="{{ value }}">
|
||||
<div class="filter-lookup-wrapper checkbox" data-value="{{ value }}">
|
||||
<label for="{{value}}">
|
||||
<input type="checkbox"
|
||||
class="product-filter field-filter"
|
||||
@@ -329,17 +329,17 @@
|
||||
{%- macro attribute_filter_section(filters)-%}
|
||||
{% for attribute in filters %}
|
||||
<div class="mb-4 filter-block pb-5">
|
||||
<div class="filter-label mb-3">{{ attribute.name}}</div>
|
||||
{% if values | len > 20 %}
|
||||
<div class="filter-label mb-3">{{ attribute.name }}</div>
|
||||
{% if attribute.item_attribute_values | len > 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 %}
|
||||
|
||||
{% if attribute.item_attribute_values %}
|
||||
<div class="filter-options">
|
||||
{% for attr_value in attribute.item_attribute_values %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<div class="filter-lookup-wrapper checkbox" data-value="{{ attr_value }}">
|
||||
<label data-value="{{ attr_value }}">
|
||||
<input type="checkbox"
|
||||
class="product-filter attribute-filter"
|
||||
id="{{ attr_value }}"
|
||||
|
||||
@@ -31,24 +31,6 @@
|
||||
{% endif %}
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user