Merge branch 'version-13-hotfix' into backport-fix-selling-settings

This commit is contained in:
Saqib
2021-09-16 16:59:54 +05:30
committed by GitHub
104 changed files with 6204 additions and 4398 deletions

View File

@@ -19,3 +19,16 @@ rules:
languages: [python]
severity: ERROR
- id: frappe-translated-values-in-business-logic
paths:
include:
- "**/report"
patterns:
- pattern-inside: |
{..., filters: [...], ...}
- pattern: |
{..., options: [..., __("..."), ...], ...}
message: |
Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
languages: [javascript]
severity: ERROR

8
.snyk
View File

@@ -1,8 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
SNYK-JS-LODASH-450202:
- cypress > getos > async > lodash:
patched: '2020-01-31T01:35:12.802Z'

View File

@@ -13,7 +13,7 @@ def get_data():
},
{
'label': _('References'),
'items': ['Period Closing Voucher', 'Tax Withholding Category']
'items': ['Period Closing Voucher']
},
{
'label': _('Target Details'),

View File

@@ -219,6 +219,7 @@
},
{
"default": "1",
"description": "A customer must have primary contact email.",
"fieldname": "primary_mandatory",
"fieldtype": "Check",
"label": "Send To Primary Contact"
@@ -286,7 +287,7 @@
}
],
"links": [],
"modified": "2021-05-21 10:14:22.426672",
"modified": "2021-09-06 21:00:45.732505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -196,7 +196,10 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
primary_email = customer.get('email_id') or ''
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
if int(primary_mandatory):
if (primary_email == ''):
continue
elif (billing_email == '') and (primary_email == ''):
continue
customer_list.append({
@@ -208,10 +211,29 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
""" Returns first email from Contact Email table as a Billing email
when Is Billing Contact checked
and Primary email- email with Is Primary checked """
billing_email = frappe.db.sql("""
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
order by c.creation desc""", customer_name)
SELECT
email.email_id
FROM
`tabContact Email` AS email
JOIN
`tabDynamic Link` AS link
ON
email.parent=link.parent
JOIN
`tabContact` AS contact
ON
contact.name=link.parent
WHERE
link.link_doctype='Customer'
and link.link_name=%s
and contact.is_billing_contact=1
ORDER BY
contact.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:

View File

@@ -1128,10 +1128,11 @@ class TestPurchaseInvoice(unittest.TestCase):
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
# Update tax withholding category with current fiscal year and rate details
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC')
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
posting_date='2021-09-15')
po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
@@ -1203,16 +1204,20 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
def update_tax_witholding_category(company, account, date):
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(date=date, company=company)
fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021')
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
'to_date': ('<=', fiscal_year[2])}):
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
tds_category.set('rates', [])
tds_category.append('rates', {
'fiscal_year': fiscal_year[0],
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 2500,
'cumulative_threshold': 0

View File

@@ -696,7 +696,6 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Scan Barcode",
"length": 1,
"options": "Barcode"
},
{
@@ -2033,7 +2032,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-08-27 20:13:40.456462",
"modified": "2021-09-08 15:24:25.486499",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -396,6 +396,7 @@ class Subscription(Document):
invoice.to_date = self.current_invoice_end
invoice.flags.ignore_mandatory = True
invoice.set_missing_values()
invoice.save()
if self.submit_invoice:

View File

@@ -9,11 +9,26 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, getdate
from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
pass
def validate(self):
self.validate_dates()
self.validate_thresholds()
def validate_dates(self):
last_date = None
for d in self.get('rates'):
if getdate(d.from_date) >= getdate(d.to_date):
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
# validate overlapping of dates
if last_date and getdate(d.to_date) < getdate(last_date):
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
def validate_thresholds(self):
for d in self.get('rates'):
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx))
def get_party_details(inv):
party_type, party = '', ''
@@ -52,8 +67,8 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if not parties:
parties.append(party)
fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
posting_date = inv.get('posting_date') or inv.get('transaction_date')
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
@@ -67,7 +82,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
tax_amount, tax_deducted = get_tax_amount(
party_type, parties,
inv, tax_details,
fiscal_year, pan_no
posting_date, pan_no
)
if party_type == 'Supplier':
@@ -77,16 +92,18 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
return tax_row
def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
tax_rate_detail = get_tax_withholding_rates(tax_withholding, fiscal_year)
tax_rate_detail = get_tax_withholding_rates(tax_withholding, posting_date)
for account_detail in tax_withholding.accounts:
if company == account_detail.company:
return frappe._dict({
"account_head": account_detail.account,
"rate": tax_rate_detail.tax_withholding_rate,
"from_date": tax_rate_detail.from_date,
"to_date": tax_rate_detail.to_date,
"threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
@@ -95,13 +112,13 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
"round_off_tax_amount": tax_withholding.round_off_tax_amount
})
def get_tax_withholding_rates(tax_withholding, fiscal_year):
def get_tax_withholding_rates(tax_withholding, posting_date):
# returns the row that matches with the fiscal year from posting date
for rate in tax_withholding.rates:
if rate.fiscal_year == fiscal_year:
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
return rate
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
frappe.throw(_("No Tax Withholding data found for the current posting date."))
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
row = {
@@ -143,38 +160,38 @@ def get_tax_row_for_tds(tax_details, tax_amount):
"account_head": tax_details.account_head
}
def get_lower_deduction_certificate(fiscal_year, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
def get_lower_deduction_certificate(tax_details, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
{
'pan_no': pan_no,
'valid_from': ('>=', tax_details.from_date),
'valid_upto': ('<=', tax_details.to_date)
}, 'name')
if ldc_name:
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0]
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
to_date=tax_details.to_date, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
tax_deducted = 0
if taxable_vouchers:
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
tax_amount = 0
posting_date = inv.get('posting_date') or inv.get('transaction_date')
if party_type == 'Supplier':
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted:
net_total = inv.net_total
if ldc:
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total)
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
else:
tax_amount = get_tds_amount(
ldc, parties, inv, tax_details,
fiscal_year_details, tax_deducted, vouchers
)
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
elif party_type == 'Customer':
if tax_deducted:
@@ -183,14 +200,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(
parties, inv, tax_details,
fiscal_year_details, vouchers, advance_vouchers
)
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
filters = {
@@ -198,14 +212,14 @@ def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
'company': company,
'party_type': party_type,
'party': ['in', parties],
'fiscal_year': fiscal_year,
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'is_opening': 'No',
'is_cancelled': 0
}
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'):
# for advance vouchers, debit and credit is reversed
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
@@ -218,8 +232,6 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
'against_voucher': ['is', 'not set']
}
if fiscal_year:
filters['fiscal_year'] = fiscal_year
if company:
filters['company'] = company
if from_date and to_date:
@@ -227,20 +239,21 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
'is_cancelled': 0,
'credit': ['>', 0],
'fiscal_year': fiscal_year,
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'account': tax_details.account_head,
'voucher_no': ['in', taxable_vouchers],
}
field = "sum(credit)"
field = "credit"
return frappe.db.get_value('GL Entry', filters, field) or 0.0
entries = frappe.db.get_all('GL Entry', filters, pluck=field)
return sum(entries)
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
@@ -264,7 +277,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company)
supp_credit_amt -= debit_note_amount
threshold = tax_details.get('threshold', 0)
@@ -292,9 +305,8 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
return tds_amount
def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0
fiscal_year, _, _ = fiscal_year_details
# sum of debit entries made from sales invoices
invoiced_amt = frappe.db.get_value('GL Entry', {
@@ -313,14 +325,14 @@ def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv
}, 'sum(credit)') or 0.0
# sum of credit entries made from sales invoice
credit_note_amt = frappe.db.get_value('GL Entry', {
credit_note_amt = sum(frappe.db.get_all('GL Entry', {
'is_cancelled': 0,
'credit': ['>', 0],
'party': ['in', parties],
'fiscal_year': fiscal_year,
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'company': inv.company,
'voucher_type': 'Sales Invoice',
}, 'sum(credit)') or 0.0
}, pluck='credit'))
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
@@ -339,7 +351,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total):
tds_amount = 0
limit_consumed = frappe.db.get_value('Purchase Invoice', {
'supplier': ('in', parties),
@@ -356,14 +368,13 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
return tds_amount
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
_, year_start_date, year_end_date = fiscal_year_details
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
filters = {
'supplier': ['in', suppliers],
'is_return': 1,
'docstatus': 1,
'posting_date': ['between', (year_start_date, year_end_date)]
'posting_date': ['between', (from_date, to_date)]
}
fields = ['abs(sum(net_total)) as net_total']

View File

@@ -313,16 +313,16 @@ def create_records():
}).insert()
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
# Cummulative thresold
fiscal_year = get_fiscal_year(today(), company="_Test Company")
# Cumulative threshold
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "Cumulative Threshold TDS",
"category_name": "10% TDS",
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000.00
@@ -339,7 +339,8 @@ def create_tax_with_holding_category():
"name": "Cumulative Threshold TCS",
"category_name": "10% TCS",
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000.00
@@ -357,7 +358,8 @@ def create_tax_with_holding_category():
"name": "Single Threshold TDS",
"category_name": "10% TDS",
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 20000.00,
'cumulative_threshold': 0
@@ -377,7 +379,8 @@ def create_tax_with_holding_category():
"consider_party_ledger_amount": 1,
"tax_on_excess_amount": 1,
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000

View File

@@ -1,202 +1,72 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-07-17 16:53:13.716665",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2018-07-17 16:53:13.716665",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"from_date",
"to_date",
"tax_withholding_rate",
"column_break_3",
"single_threshold",
"cumulative_threshold"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "fiscal_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fiscal Year",
"length": 0,
"no_copy": 0,
"options": "Fiscal Year",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 1,
"fieldname": "tax_withholding_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Withholding Rate",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "tax_withholding_rate",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Withholding Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Single Transaction Threshold"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Single Transaction Threshold",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Cumulative Transaction Threshold",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"columns": 2,
"fieldname": "from_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "From Date",
"reqd": 1
},
{
"columns": 2,
"fieldname": "to_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "To Date",
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-07-17 17:13:09.819580",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-31 11:42:12.213977",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -4,7 +4,7 @@
frappe.query_reports["Accounts Payable"] = {
"filters": [
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@@ -12,19 +12,19 @@ frappe.query_reports["Accounts Payable"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"fieldname": "report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"finance_book",
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@@ -38,7 +38,7 @@ frappe.query_reports["Accounts Payable"] = {
}
},
{
"fieldname":"supplier",
"fieldname": "supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
@@ -54,48 +54,48 @@ frappe.query_reports["Accounts Payable"] = {
}
},
{
"fieldname":"ageing_based_on",
"fieldname": "ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
"default": "Due Date"
},
{
"fieldname":"range1",
"fieldname": "range1",
"label": __("Ageing Range 1"),
"fieldtype": "Int",
"default": "30",
"reqd": 1
},
{
"fieldname":"range2",
"fieldname": "range2",
"label": __("Ageing Range 2"),
"fieldtype": "Int",
"default": "60",
"reqd": 1
},
{
"fieldname":"range3",
"fieldname": "range3",
"label": __("Ageing Range 3"),
"fieldtype": "Int",
"default": "90",
"reqd": 1
},
{
"fieldname":"range4",
"fieldname": "range4",
"label": __("Ageing Range 4"),
"fieldtype": "Int",
"default": "120",
"reqd": 1
},
{
"fieldname":"payment_terms_template",
"fieldname": "payment_terms_template",
"label": __("Payment Terms Template"),
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"supplier_group",
"fieldname": "supplier_group",
"label": __("Supplier Group"),
"fieldtype": "Link",
"options": "Supplier Group"
@@ -106,12 +106,17 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"fieldname": "based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname":"tax_id",
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1

View File

@@ -4,7 +4,7 @@
frappe.query_reports["Accounts Receivable"] = {
"filters": [
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@@ -12,19 +12,19 @@ frappe.query_reports["Accounts Receivable"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"fieldname": "report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"finance_book",
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@@ -38,7 +38,7 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"customer",
"fieldname": "customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer",
@@ -67,66 +67,66 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"ageing_based_on",
"fieldname": "ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date',
"default": "Due Date"
},
{
"fieldname":"range1",
"fieldname": "range1",
"label": __("Ageing Range 1"),
"fieldtype": "Int",
"default": "30",
"reqd": 1
},
{
"fieldname":"range2",
"fieldname": "range2",
"label": __("Ageing Range 2"),
"fieldtype": "Int",
"default": "60",
"reqd": 1
},
{
"fieldname":"range3",
"fieldname": "range3",
"label": __("Ageing Range 3"),
"fieldtype": "Int",
"default": "90",
"reqd": 1
},
{
"fieldname":"range4",
"fieldname": "range4",
"label": __("Ageing Range 4"),
"fieldtype": "Int",
"default": "120",
"reqd": 1
},
{
"fieldname":"customer_group",
"fieldname": "customer_group",
"label": __("Customer Group"),
"fieldtype": "Link",
"options": "Customer Group"
},
{
"fieldname":"payment_terms_template",
"fieldname": "payment_terms_template",
"label": __("Payment Terms Template"),
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"sales_partner",
"fieldname": "sales_partner",
"label": __("Sales Partner"),
"fieldtype": "Link",
"options": "Sales Partner"
},
{
"fieldname":"sales_person",
"fieldname": "sales_person",
"label": __("Sales Person"),
"fieldtype": "Link",
"options": "Sales Person"
},
{
"fieldname":"territory",
"fieldname": "territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
@@ -137,45 +137,50 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"fieldname": "based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname":"show_future_payments",
"fieldname": "show_future_payments",
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname":"show_delivery_notes",
"fieldname": "show_delivery_notes",
"label": __("Show Linked Delivery Notes"),
"fieldtype": "Check",
},
{
"fieldname":"show_sales_person",
"fieldname": "show_sales_person",
"label": __("Show Sales Person"),
"fieldtype": "Check",
},
{
"fieldname":"tax_id",
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"customer_name",
"fieldname": "customer_name",
"label": __("Customer Name"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"payment_terms",
"fieldname": "payment_terms",
"label": __("Payment Tems"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"credit_limit",
"fieldname": "credit_limit",
"label": __("Credit Limit"),
"fieldtype": "Currency",
"hidden": 1

View File

@@ -106,6 +106,7 @@ class ReceivablePayableReport(object):
party = gle.party,
posting_date = gle.posting_date,
account_currency = gle.account_currency,
remarks = gle.remarks if self.filters.get("show_remarks") else None,
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
@@ -583,10 +584,12 @@ class ReceivablePayableReport(object):
else:
select_fields = "debit, credit"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0}
against_voucher_type, against_voucher, account_currency, {0} {remarks}
from
`tabGL Entry`
where
@@ -595,7 +598,7 @@ class ReceivablePayableReport(object):
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
.format(select_fields, date_condition, conditions, order_by), values, as_dict=True)
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@@ -754,6 +757,10 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
options='voucher_type', width=180)
if self.filters.show_remarks:
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200),
self.add_column(label='Due Date', fieldtype='Date')
if self.party_type == "Supplier":

View File

@@ -260,7 +260,12 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values():
for entry in entries:
d = accounts_by_name.get(entry.account_name)
if entry.account_number:
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
d = accounts_by_name.get(account_name)
if d:
for company in companies:
# check if posting date is within the period
@@ -307,7 +312,14 @@ def update_parent_account_names(accounts):
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
name_to_account_map = {}
for d in accounts:
if d.account_number:
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
name_to_account_map[d.name] = account_name
for account in accounts:
if account.parent_account:
@@ -420,7 +432,11 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries:
account_name = entry.account_name
if entry.account_number:
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(account_name, []).append(entry)
@@ -491,7 +507,12 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
accounts_by_name[d.account_name] = d
if d.account_number:
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = []

View File

@@ -110,9 +110,26 @@ frappe.query_reports["General Ledger"] = {
"fieldname":"group_by",
"label": __("Group by"),
"fieldtype": "Select",
"options": ["", __("Group by Voucher"), __("Group by Voucher (Consolidated)"),
__("Group by Account"), __("Group by Party")],
"default": __("Group by Voucher (Consolidated)")
"options": [
"",
{
label: __("Group by Voucher"),
value: "Group by Voucher",
},
{
label: __("Group by Voucher (Consolidated)"),
value: "Group by Voucher (Consolidated)",
},
{
label: __("Group by Account"),
value: "Group by Account",
},
{
label: __("Group by Party"),
value: "Group by Party",
},
],
"default": "Group by Voucher (Consolidated)"
},
{
"fieldname":"tax_id",

View File

@@ -62,14 +62,14 @@ def validate_filters(filters, account_details):
if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
if (filters.get("account") and filters.get("group_by") == 'Group by Account'):
filters.account = frappe.parse_json(filters.get('account'))
for account in filters.account:
if account_details[account].is_group == 0:
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if (filters.get("voucher_no")
and filters.get("group_by") in [_('Group by Voucher')]):
and filters.get("group_by") in ['Group by Voucher']):
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
if filters.from_date > filters.to_date:
@@ -153,7 +153,7 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("include_dimensions"):
order_by_statement = "order by posting_date, creation"
if filters.get("group_by") == _("Group by Voucher"):
if filters.get("group_by") == "Group by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no"
if filters.get("include_default_book_entries"):
@@ -312,13 +312,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account
data.append(totals.opening)
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
for acc, acc_dict in iteritems(gle_map):
# acc
if acc_dict.entries:
# opening
data.append({})
if filters.get("group_by") != _("Group by Voucher"):
if filters.get("group_by") != "Group by Voucher":
data.append(acc_dict.totals.opening)
data += acc_dict.entries
@@ -327,7 +327,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
data.append(acc_dict.totals.total)
# closing
if filters.get("group_by") != _("Group by Voucher"):
if filters.get("group_by") != "Group by Voucher":
data.append(acc_dict.totals.closing)
data.append({})
else:
@@ -357,9 +357,9 @@ def get_totals_dict():
)
def group_by_field(group_by):
if group_by == _('Group by Party'):
if group_by == 'Group by Party':
return 'party'
elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]:
elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']:
return 'account'
else:
return 'voucher_no'
@@ -423,9 +423,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif gle.posting_date <= to_date:
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))

View File

@@ -4,9 +4,10 @@
frappe.query_reports["Unpaid Expense Claim"] = {
"filters": [
{
"fieldname":"employee",
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link"
"fieldtype": "Link",
"options": "Employee"
}
]
}

View File

@@ -28,7 +28,7 @@
"fieldname": "supp_master_name",
"fieldtype": "Select",
"label": "Supplier Naming By",
"options": "Supplier Name\nNaming Series"
"options": "Supplier Name\nNaming Series\nAuto Name"
},
{
"fieldname": "supplier_group",
@@ -123,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-06-24 10:38:28.934525",
"modified": "2021-09-08 19:26:23.548837",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
status: ["!=", "Stopped"],
per_ordered: ["<", 100],
company: me.frm.doc.company
}
},
allow_child_item_selection: true,
child_fielname: "items",
child_columns: ["item_code", "qty"]
})
}, __("Get Items From"));

View File

@@ -394,12 +394,10 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
@frappe.whitelist()
def get_supplier_tag():
if not frappe.cache().hget("Supplier", "Tags"):
filters = {"document_type": "Supplier"}
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
frappe.cache().hset("Supplier", "Tags", tags)
filters = {"document_type": "Supplier"}
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
return frappe.cache().hget("Supplier", "Tags")
return tags
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@@ -433,12 +433,12 @@
"image_field": "image",
"links": [
{
"group": "Item Group",
"link_doctype": "Supplier Item Group",
"link_fieldname": "supplier"
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-08-27 18:02:44.314077",
"modified": "2021-09-06 17:37:56.522233",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -10,7 +10,7 @@ from frappe.contacts.address_and_contact import (
delete_contact_and_address,
load_address_and_contact,
)
from frappe.model.naming import set_name_by_naming_series
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
from erpnext.utilities.transaction_base import TransactionBase
@@ -40,8 +40,10 @@ class Supplier(TransactionBase):
supp_master_name = frappe.defaults.get_global_default('supp_master_name')
if supp_master_name == 'Supplier Name':
self.name = self.supplier_name
else:
elif supp_master_name == 'Naming Series':
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def on_update(self):
if not self.naming_series:

View File

@@ -1,77 +0,0 @@
{
"actions": [],
"creation": "2021-05-07 18:16:40.621421",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"supplier",
"item_group"
],
"fields": [
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier",
"reqd": 1
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Group",
"options": "Item Group",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-19 13:48:16.742303",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Item Group",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class SupplierItemGroup(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Supplier Item Group',
'supplier': self.supplier,
'item_group': self.item_group
})
if exists:
frappe.throw(_("Item Group has already been linked to this supplier."))

View File

@@ -7,6 +7,7 @@ import json
from collections import defaultdict
import frappe
from frappe import scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.utils import nowdate, unique
@@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict) and filters.get('supplier'):
item_group_list = frappe.get_all('Supplier Item Group',
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
if filters and isinstance(filters, dict):
if filters.get('customer') or filters.get('supplier'):
party = filters.get('customer') or filters.get('supplier')
item_rules_list = frappe.get_all('Party Specific Item',
filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value'])
item_groups = []
for i in item_group_list:
item_groups.append(i.item_group)
filters_dict = {}
for rule in item_rules_list:
if rule['restrict_based_on'] == 'Item':
rule['restrict_based_on'] = 'name'
filters_dict[rule.restrict_based_on] = []
del filters['supplier']
for rule in item_rules_list:
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
for filter in filters_dict:
filters[scrub(filter)] = ['in', filters_dict[filter]]
if filters.get('customer'):
del filters['customer']
else:
del filters['supplier']
if item_groups:
filters['item_group'] = ['in', item_groups]
description_cond = ''
if frappe.db.count('Item', cache=True) < 50000:
@@ -307,7 +319,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
@frappe.validate_and_sanitize_search_inputs
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
cond = ''
if filters.get('customer'):
if filters and filters.get('customer'):
cond = """(`tabProject`.customer = %s or
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))

View File

@@ -0,0 +1,87 @@
import unittest
from functools import partial
from erpnext.controllers import queries
def add_default_params(func, doctype):
return partial(
func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None
)
class TestQueries(unittest.TestCase):
# All tests are based on doctype/test_records.json
def assert_nested_in(self, item, container):
self.assertIn(item, [vals for tuples in container for vals in tuples])
def test_employee_query(self):
query = add_default_params(queries.employee_query, "Employee")
self.assertGreaterEqual(len(query(txt="_Test Employee")), 3)
self.assertGreaterEqual(len(query(txt="_Test Employee 1")), 1)
def test_lead_query(self):
query = add_default_params(queries.lead_query, "Lead")
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
def test_customer_query(self):
query = add_default_params(queries.customer_query, "Customer")
self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
def test_supplier_query(self):
query = add_default_params(queries.supplier_query, "Supplier")
self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
def test_item_query(self):
query = add_default_params(queries.item_query, "Item")
self.assertGreaterEqual(len(query(txt="_Test Item")), 7)
self.assertEqual(len(query(txt="_Test Item Home Desktop 100 3")), 1)
fg_item = "_Test FG Item"
stock_items = query(txt=fg_item, filters={"is_stock_item": 1})
self.assert_nested_in("_Test FG Item", stock_items)
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
self.assertEqual(len(bundled_stock_items), 0)
def test_bom_qury(self):
query = add_default_params(queries.bom, "BOM")
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
def test_project_query(self):
query = add_default_params(queries.get_project_name, "BOM")
self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
def test_account_query(self):
query = add_default_params(queries.get_account_list, "Account")
debtor_accounts = query(txt="Debtors", filters={"company": "_Test Company"})
self.assert_nested_in("Debtors - _TC", debtor_accounts)
def test_income_account_query(self):
query = add_default_params(queries.get_income_account, "Account")
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
def test_expense_account_query(self):
query = add_default_params(queries.get_expense_account, "Account")
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
def test_warehouse_query(self):
query = add_default_params(queries.warehouse_query, "Account")
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)

View File

@@ -127,7 +127,7 @@ class ECommerceSettings(Document):
if not (new_fields == old_fields):
create_website_items_index()
def validate_cart_settings(doc, method):
def validate_cart_settings(doc=None, method=None):
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
def get_shopping_cart_settings():

View File

@@ -12,8 +12,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
from frappe.website.website_generator import WebsiteGenerator
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
# SEARCH
from erpnext.e_commerce.redisearch import (
delete_item_from_index,
insert_item_to_index,
@@ -138,10 +136,10 @@ class WebsiteItem(WebsiteGenerator):
self.website_image = None
def make_thumbnail(self):
if frappe.flags.in_import:
"""Make a thumbnail of `website_image`"""
if frappe.flags.in_import or frappe.flags.in_migrate:
return
"""Make a thumbnail of `website_image`"""
import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):

View File

@@ -105,7 +105,7 @@ def place_order():
if is_stock_item:
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
throw(_("{1} Not in Stock").format(item.item_code))
throw(_("{0} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty[0][0]:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
@@ -168,8 +168,10 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
return {
"items": frappe.render_template("templates/includes/cart/cart_items.html",
context),
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
"total": frappe.render_template("templates/includes/cart/cart_items_total.html",
context),
"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
context)
}
else:
return {

View File

@@ -67,12 +67,16 @@ class ItemVariantsCacheManager:
as_list=1
)
disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})])
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
attribute_value_item_map = frappe._dict({})
item_attribute_value_map = frappe._dict({})
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
# dont consider variants that are unpublished
# (either have no Website Item or are unpublished in Website Item)
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
for row in item_variants_data:
item_code, attribute, attribute_value = row
# (attr, value) => [item1, item2]

View File

@@ -4,6 +4,7 @@ import frappe
import taxjar
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
from frappe.utils import cint
from erpnext import get_default_company
@@ -14,6 +15,10 @@ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_cal
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"]
SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL',
'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE',
'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD',
'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY']
def get_client():
@@ -27,7 +32,11 @@ def get_client():
api_url = taxjar.SANDBOX_API_URL
if api_key and api_url:
return taxjar.Client(api_key=api_key, api_url=api_url)
client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', {
'x-api-version': '2020-08-07'
})
return client
def create_transaction(doc, method):
@@ -57,7 +66,10 @@ def create_transaction(doc, method):
tax_dict['amount'] = doc.total + tax_dict['shipping']
try:
client.create_order(tax_dict)
if doc.is_return:
client.create_refund(tax_dict)
else:
client.create_order(tax_dict)
except taxjar.exceptions.TaxJarResponseError as err:
frappe.throw(_(sanitize_error_response(err)))
except Exception as ex:
@@ -89,13 +101,15 @@ def get_tax_data(doc):
to_country_code = frappe.db.get_value("Country", to_address.country, "code")
to_country_code = to_country_code.upper()
if to_country_code not in SUPPORTED_COUNTRY_CODES:
return
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
if to_shipping_state is not None:
to_shipping_state = get_iso_3166_2_state_code(to_address)
line_items = [get_line_item_dict(item) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company')
if to_shipping_state not in SUPPORTED_STATE_CODES:
to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = {
'from_country': from_country_code,
@@ -109,11 +123,29 @@ def get_tax_data(doc):
'to_street': to_address.address_line1,
'to_state': to_shipping_state,
'shipping': shipping,
'amount': doc.net_total
'amount': doc.net_total,
'plugin': 'erpnext',
'line_items': line_items
}
return tax_dict
def get_state_code(address, location):
if address is not None:
state_code = get_iso_3166_2_state_code(address)
if state_code not in SUPPORTED_STATE_CODES:
frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
else:
frappe.throw(_("Please enter a valid State in the {0} Address").format(location))
return state_code
def get_line_item_dict(item):
return dict(
id = item.get('idx'),
quantity = item.get('qty'),
unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category')
)
def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX:
@@ -122,17 +154,7 @@ def set_sales_tax(doc, method):
if not doc.items:
return
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
if sales_tax_exempted:
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
tax.tax_amount = 0
break
doc.run_method("calculate_taxes_and_totals")
if check_sales_tax_exemption(doc):
return
tax_dict = get_tax_data(doc)
@@ -143,7 +165,6 @@ def set_sales_tax(doc, method):
return
tax_data = validate_tax_request(tax_dict)
if tax_data is not None:
if not tax_data.amount_to_collect:
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
@@ -163,9 +184,28 @@ def set_sales_tax(doc, method):
"account_head": TAX_ACCOUNT_HEAD,
"tax_amount": tax_data.amount_to_collect
})
# Assigning values to tax_collectable and taxable_amount fields in sales item table
for item in tax_data.breakdown.line_items:
doc.get('items')[cint(item.id)-1].tax_collectable = item.tax_collectable
doc.get('items')[cint(item.id)-1].taxable_amount = item.taxable_amount
doc.run_method("calculate_taxes_and_totals")
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
if sales_tax_exempted:
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
tax.tax_amount = 0
break
doc.run_method("calculate_taxes_and_totals")
return True
else:
return False
def validate_tax_request(tax_dict):
"""Return the sales tax that should be collected for a given order."""
@@ -200,6 +240,8 @@ def get_shipping_address_details(doc):
if doc.shipping_address_name:
shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
elif doc.customer_address:
shipping_address = frappe.get_doc("Address", doc.customer_address_name)
else:
shipping_address = get_company_address_details(doc)

View File

@@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', {
frm.trigger('make_return_entry');
}, __('Create'));
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
frm.add_custom_button(__("Deduction from salary"), function() {
frm.add_custom_button(__("Deduction from Salary"), function() {
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}

View File

@@ -170,7 +170,7 @@
"default": "0",
"fieldname": "repay_unclaimed_amount_from_salary",
"fieldtype": "Check",
"label": "Repay unclaimed amount from salary"
"label": "Repay Unclaimed Amount from Salary"
},
{
"depends_on": "eval:cur_frm.doc.employee",
@@ -200,7 +200,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 22:31:53.746659",
"modified": "2021-09-11 18:38:38.617478",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",

View File

@@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
@frappe.whitelist()
def create_return_through_additional_salary(doc):
import json
doc = frappe._dict(json.loads(doc))
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = doc.employee
additional_salary.currency = doc.currency

View File

@@ -12,8 +12,11 @@ import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.employee_advance.employee_advance import (
EmployeeAdvanceOverPayment,
create_return_through_additional_salary,
make_bank_entry,
)
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
class TestEmployeeAdvance(unittest.TestCase):
@@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase):
journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
args = {"type": "Deduction"}
create_salary_component("Advance Salary - Deduction", **args)
make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name)
# additional salary for 700 first
advance.reload()
additional_salary = create_return_through_additional_salary(advance)
additional_salary.salary_component = "Advance Salary - Deduction"
additional_salary.payroll_date = nowdate()
additional_salary.amount = 700
additional_salary.insert()
additional_salary.submit()
advance.reload()
self.assertEqual(advance.return_amount, 700)
# additional salary for remaining 300
additional_salary = create_return_through_additional_salary(advance)
additional_salary.salary_component = "Advance Salary - Deduction"
additional_salary.payroll_date = nowdate()
additional_salary.amount = 300
additional_salary.insert()
additional_salary.submit()
advance.reload()
self.assertEqual(advance.return_amount, 1000)
# update advance return amount on additional salary cancellation
additional_salary.cancel()
advance.reload()
self.assertEqual(advance.return_amount, 700)
def tearDown(self):
frappe.db.rollback()
def make_payment_entry(advance):
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
journal_entry.cheque_no = "123123"
@@ -41,7 +84,7 @@ def make_payment_entry(advance):
return journal_entry
def make_employee_advance(employee_name):
def make_employee_advance(employee_name, args=None):
doc = frappe.new_doc("Employee Advance")
doc.employee = employee_name
doc.company = "_Test company"
@@ -51,6 +94,10 @@ def make_employee_advance(employee_name):
doc.advance_amount = 1000
doc.posting_date = nowdate()
doc.advance_account = "_Test Employee Advance - _TC"
if args:
doc.update(args)
doc.insert()
doc.submit()

View File

@@ -148,7 +148,10 @@ def set_employee_name(doc):
def update_employee(employee, details, date=None, cancel=False):
internal_work_history = {}
for item in details:
fieldtype = frappe.get_meta("Employee").get_field(item.fieldname).fieldtype
field = frappe.get_meta("Employee").get_field(item.fieldname)
if not field:
continue
fieldtype = field.fieldtype
new_data = item.new if not cancel else item.current
if fieldtype == "Date" and new_data:
new_data = getdate(new_data)

View File

@@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
},
refresh: function (frm) {
setTimeout(() => {
frm.toggle_display('generate_schedule', !(frm.is_new()));
frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus));
frm.toggle_display('schedule', !(frm.is_new()));
}, 10);
},

View File

@@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
class MaintenanceSchedule(TransactionBase):
@frappe.whitelist()
def generate_schedule(self):
if self.docstatus != 0:
return
self.set('schedules', [])
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
where parent=%s""", (self.name))
count = 1
for d in self.get('items'):
self.validate_maintenance_detail()

View File

@@ -31,8 +31,8 @@ frappe.ui.form.on('Maintenance Visit', {
},
onload: function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (frm.maintenance_type == 'Scheduled') {
let schedule_id = item.purposes[0].prevdoc_detail_docname;
if (frm.doc.maintenance_type === "Scheduled") {
const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail;
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
args: {
@@ -43,6 +43,9 @@ frappe.ui.form.on('Maintenance Visit', {
}
});
}
else {
frm.clear_table("purposes");
}
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });

View File

@@ -511,8 +511,14 @@ class BOM(WebsiteGenerator):
if d.workstation:
self.update_rate_and_time(d, update_hour_rate)
self.operating_cost += flt(d.operating_cost)
self.base_operating_cost += flt(d.base_operating_cost)
operating_cost = d.operating_cost
base_operating_cost = d.base_operating_cost
if d.set_cost_based_on_bom_qty:
operating_cost = flt(d.cost_per_unit) * flt(self.quantity)
base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity)
self.operating_cost += flt(operating_cost)
self.base_operating_cost += flt(base_operating_cost)
def update_rate_and_time(self, row, update_hour_rate = False):
if not row.hour_rate or update_hour_rate:
@@ -536,6 +542,8 @@ class BOM(WebsiteGenerator):
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
if update_hour_rate:
row.db_update()

View File

@@ -107,6 +107,24 @@ class TestBOM(unittest.TestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0
op_cost = 0.0
for op_row in bom.operations:
op_row.docstatus = 0
op_row.batch_size = 2
op_row.set_cost_based_on_bom_qty = 1
op_cost += op_row.operating_cost
bom.save()
for op_row in bom.operations:
self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2)
self.assertAlmostEqual(bom.operating_cost, op_cost/2)
bom.delete()
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):

View File

@@ -8,15 +8,23 @@
"field_order": [
"sequence_id",
"operation",
"workstation",
"description",
"col_break1",
"hour_rate",
"workstation",
"time_in_mins",
"operating_cost",
"costing_section",
"hour_rate",
"base_hour_rate",
"column_break_9",
"operating_cost",
"base_operating_cost",
"column_break_11",
"batch_size",
"set_cost_based_on_bom_qty",
"cost_per_unit",
"base_cost_per_unit",
"more_information_section",
"description",
"column_break_18",
"image"
],
"fields": [
@@ -117,13 +125,59 @@
"fieldname": "sequence_id",
"fieldtype": "Int",
"label": "Sequence ID"
},
{
"depends_on": "eval:doc.batch_size > 0 && doc.set_cost_based_on_bom_qty",
"fieldname": "cost_per_unit",
"fieldtype": "Float",
"label": "Cost Per Unit",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "base_cost_per_unit",
"fieldtype": "Float",
"hidden": 1,
"label": "Base Cost Per Unit",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "costing_section",
"fieldtype": "Section Break",
"label": "Costing"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "set_cost_based_on_bom_qty",
"fieldtype": "Check",
"label": "Set Operating Cost Based On BOM Quantity"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-12 14:48:09.596843",
"modified": "2021-09-13 16:45:01.092868",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -26,15 +26,23 @@ frappe.ui.form.on('Job Card', {
refresh: function(frm) {
frappe.flags.pause_job = 0;
frappe.flags.resume_job = 0;
let has_items = frm.doc.items && frm.doc.items.length;
if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) {
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
if (to_request || excess_transfer_allowed) {
frm.add_custom_button(__("Material Request"), () => {
frm.trigger("make_material_request");
});
}
if (frm.doc.for_quantity != frm.doc.transferred_qty) {
// check if any row has untransferred materials
// in case of multiple items in JC
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry");
}).addClass("btn-primary");

View File

@@ -38,6 +38,8 @@
"total_time_in_mins",
"section_break_8",
"items",
"scrap_items_section",
"scrap_items",
"corrective_operation_section",
"for_job_card",
"is_corrective_job_card",
@@ -185,7 +187,7 @@
"default": "0",
"fieldname": "transferred_qty",
"fieldtype": "Float",
"label": "Transferred Qty",
"label": "FG Qty from Transferred Raw Materials",
"read_only": 1
},
{
@@ -392,14 +394,28 @@
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
},
{
"fieldname": "scrap_items_section",
"fieldtype": "Section Break",
"label": "Scrap Items"
},
{
"fieldname": "scrap_items",
"fieldtype": "Table",
"label": "Scrap Items",
"no_copy": 1,
"options": "Job Card Scrap Item",
"print_hide": 1
}
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-16 15:59:32.766484",
"modified": "2021-09-14 00:38:46.873105",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import datetime
import json
@@ -37,6 +34,10 @@ class OperationSequenceError(frappe.ValidationError): pass
class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document):
def onload(self):
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
self.set_onload("job_card_excess_transfer", excess_transfer)
def validate(self):
self.validate_time_logs()
self.set_status()
@@ -91,7 +92,7 @@ class JobCard(Document):
if args.get("employee"):
# override capacity for employee
production_capacity = 1
validate_overlap_for = " and jc.employee = %(employee)s "
validate_overlap_for = " and jctl.employee = %(employee)s "
extra_cond = ''
if check_next_available_slot:
@@ -449,6 +450,7 @@ class JobCard(Document):
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
def set_transferred_qty(self, update_status=False):
"Set total FG Qty for which RM was transferred."
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -457,6 +459,7 @@ class JobCard(Document):
return
if self.items:
# sum of 'For Quantity' of Stock Entries against JC
self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name,
'work_order': self.work_order,
@@ -500,7 +503,9 @@ class JobCard(Document):
self.status = 'Work In Progress'
if (self.docstatus == 1 and
(self.for_quantity == self.transferred_qty or not self.items)):
(self.for_quantity <= self.transferred_qty or not self.items)):
# consider excess transfer
# completed qty is checked via separate validation
self.status = 'Completed'
if self.status != 'Completed':
@@ -618,7 +623,11 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
# avoid negative 'For Quantity'
pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()

View File

@@ -1,78 +1,194 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import random_string
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
from erpnext.manufacturing.doctype.job_card.job_card import (
make_stock_entry as make_stock_entry_from_jc,
)
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestJobCard(unittest.TestCase):
def setUp(self):
transfer_material_against, source_warehouse = None, None
tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer",
"test_job_card_excess_material_transfer")
if self._testMethodName in tests_that_transfer_against_jc:
transfer_material_against = "Job Card"
source_warehouse = "Stores - _TC"
self.work_order = make_wo_order_test_record(
item="_Test FG Item 2",
qty=2,
transfer_material_against=transfer_material_against,
source_warehouse=source_warehouse
)
def tearDown(self):
frappe.db.rollback()
def test_job_card(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
job_cards = frappe.get_all('Job Card',
filters = {'work_order': self.work_order.name}, fields = ["operation_id", "name"])
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
if job_cards:
job_card = job_cards[0]
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name}, fields = ["operation_id", "name"])
doc = frappe.get_doc("Job Card", job_card.name)
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
if job_cards:
job_card = job_cards[0]
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
doc = frappe.get_doc("Job Card", job_card.name)
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
job_cards = frappe.get_all('Job Card',
filters = {'work_order': self.work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
if data:
bom, bom_item = data
job_card = job_cards[0]
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
job_card = job_cards[0]
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.submit()
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
self.assertEqual(completed_qty, job_card.for_quantity)
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.submit()
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
doc.cancel()
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
jc1_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
jc2_name = frappe.db.get_value("Job Card", {'work_order': wo2.name})
jc1 = frappe.get_doc("Job Card", jc1_name)
jc2 = frappe.get_doc("Job Card", jc2_name)
employee = "_T-Employee-00001" # from test records
jc1.append("time_logs", {
"from_time": "2021-01-01 00:00:00",
"to_time": "2021-01-01 08:00:00",
"completed_qty": 1,
"employee": employee,
})
jc1.save()
# add a new entry in same time slice
jc2.append("time_logs", {
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 1,
"employee": employee,
})
self.assertRaises(OverlapError, jc2.save)
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
make_stock_entry(
item_code="_Test Item",
target="Stores - _TC",
qty=10,
basic_rate=100
)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC",
qty=6,
basic_rate=100
)
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)
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
transfer_entry_1.insert()
transfer_entry_1.submit()
job_card.reload()
self.assertEqual(transfer_entry_1.fg_completed_qty, 2)
self.assertEqual(job_card.transferred_qty, 2)
# transfer second RM
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
del transfer_entry_2.items[0]
transfer_entry_2.insert()
transfer_entry_2.submit()
# 'For Quantity' here will be 0 since
# transfer was made for 2 fg qty in first transfer Stock Entry
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card."
make_stock_entry(item_code="_Test Item", target="Stores - _TC",
qty=25, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC", qty=15, basic_rate=100)
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)
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
transfer_entry_1.insert()
transfer_entry_1.submit()
# transfer extra qty of both RM due to previously damaged RM
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
# deliberately change 'For Quantity'
transfer_entry_2.fg_completed_qty = 1
transfer_entry_2.items[0].qty = 5
transfer_entry_2.items[1].qty = 3
transfer_entry_2.insert()
transfer_entry_2.submit()
job_card.reload()
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
# Check if 'For Quantity' is negative
# as 'transferred_qty' > Qty to Manufacture
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
job_card.append("time_logs", {
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 2
})
job_card.save()
job_card.submit()
# JC is Completed with excess transfer
self.assertEqual(job_card.status, "Completed")

View File

@@ -0,0 +1,82 @@
{
"actions": [],
"creation": "2021-09-14 00:30:28.533884",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"column_break_3",
"description",
"quantity_and_rate",
"stock_qty",
"column_break_6",
"stock_uom"
],
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Scrap Item Code",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Scrap Item Name"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"reqd": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-14 01:20:48.588052",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Scrap Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class JobCardScrapItem(Document):
pass

View File

@@ -25,9 +25,12 @@
"overproduction_percentage_for_sales_order",
"column_break_16",
"overproduction_percentage_for_work_order",
"job_card_section",
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_24",
"job_card_excess_transfer",
"other_settings_section",
"update_bom_costs_automatically",
"add_corrective_operation_cost_in_finished_good_valuation",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
@@ -96,10 +99,10 @@
},
{
"default": "0",
"description": "Allow multiple material consumptions against a Work Order",
"description": "Allow material consumptions without immediately manufacturing finished goods against a Work Order",
"fieldname": "material_consumption",
"fieldtype": "Check",
"label": "Allow Multiple Material Consumption"
"label": "Allow Continuous Material Consumption"
},
{
"default": "0",
@@ -175,13 +178,29 @@
"fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
"fieldtype": "Check",
"label": "Add Corrective Operation Cost in Finished Good Valuation"
},
{
"fieldname": "job_card_section",
"fieldtype": "Section Break",
"label": "Job Card"
},
{
"fieldname": "column_break_24",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Allow transferring raw materials even after the Required Quantity is fulfilled",
"fieldname": "job_card_excess_transfer",
"fieldtype": "Check",
"label": "Allow Excess Material Transfer"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-16 15:54:38.967341",
"modified": "2021-09-13 22:09:09.401559",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@@ -242,6 +242,8 @@ frappe.ui.form.on('Production Plan', {
},
get_sub_assembly_items: function(frm) {
frm.dirty();
frappe.call({
method: "get_sub_assembly_items",
freeze: true,
@@ -434,6 +436,25 @@ frappe.ui.form.on("Material Request Plan Item", {
}
});
frappe.ui.form.on("Production Plan Sales Order", {
sales_order(frm, cdt, cdn) {
const { sales_order } = locals[cdt][cdn];
if (!sales_order) {
return;
}
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
args: { sales_order },
callback(r) {
const {transaction_date, customer, grand_total} = r.message;
frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
frappe.model.set_value(cdt, cdn, 'customer', customer);
frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
}
});
}
});
cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() {
return{
filters: [

View File

@@ -16,10 +16,12 @@
"customer",
"warehouse",
"project",
"sales_order_status",
"column_break2",
"from_date",
"to_date",
"sales_order_status",
"from_delivery_date",
"to_delivery_date",
"sales_orders_detail",
"get_sales_orders",
"sales_orders",
@@ -358,13 +360,23 @@
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
},
{
"fieldname": "from_delivery_date",
"fieldtype": "Date",
"label": "From Delivery Date"
},
{
"fieldname": "to_delivery_date",
"fieldtype": "Date",
"label": "To Delivery Date"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-08-23 17:26:03.799876",
"modified": "2021-09-06 18:35:59.642232",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -561,8 +561,6 @@ class ProductionPlan(Document):
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
self.save()
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
@@ -735,43 +733,42 @@ def get_material_request_items(row, sales_order, company,
def get_sales_orders(self):
so_filter = item_filter = ""
bom_item = "bom.item = so_item.item_code"
if self.from_date:
so_filter += " and so.transaction_date >= %(from_date)s"
if self.to_date:
so_filter += " and so.transaction_date <= %(to_date)s"
if self.customer:
so_filter += " and so.customer = %(customer)s"
if self.project:
so_filter += " and so.project = %(project)s"
if self.sales_order_status:
so_filter += "and so.status = %(sales_order_status)s"
date_field_mapper = {
'from_date': ('>=', 'so.transaction_date'),
'to_date': ('<=', 'so.transaction_date'),
'from_delivery_date': ('>=', 'so_item.delivery_date'),
'to_delivery_date': ('<=', 'so_item.delivery_date')
}
for field, value in date_field_mapper.items():
if self.get(field):
so_filter += f" and {value[1]} {value[0]} %({field})s"
for field in ['customer', 'project', 'sales_order_status']:
if self.get(field):
so_field = 'status' if field == 'sales_order_status' else field
so_filter += f" and so.{so_field} = %({field})s"
if self.item_code and frappe.db.exists('Item', self.item_code):
bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item)s"
item_filter += " and so_item.item_code = %(item_code)s"
open_so = frappe.db.sql("""
open_so = frappe.db.sql(f"""
select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent = so.name
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {0} {1}
and (exists (select name from `tabBOM` bom where {2}
and so_item.qty > so_item.work_order_qty {so_filter} {item_filter}
and (exists (select name from `tabBOM` bom where {bom_item}
and bom.is_active = 1)
or exists (select name from `tabPacked Item` pi
where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1)))
""".format(so_filter, item_filter, bom_item), {
"from_date": self.from_date,
"to_date": self.to_date,
"customer": self.customer,
"project": self.project,
"item": self.item_code,
"company": self.company,
"sales_order_status": self.sales_order_status
}, as_dict=1)
""", self.as_dict(), as_dict=1)
return open_so
@frappe.whitelist()
@@ -800,6 +797,12 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
@frappe.whitelist()
def get_so_details(sales_order):
return frappe.db.get_value("Sales Order", sales_order,
['transaction_date', 'customer', 'grand_total'], as_dict=1
)
def get_warehouse_list(warehouses):
warehouse_list = []

View File

@@ -404,6 +404,7 @@ def make_bom(**args):
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate or args.rate,
'source_warehouse': args.source_warehouse
})
if not args.do_not_save:

View File

@@ -1,9 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe
@@ -20,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
stop_unstop,
)
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin
@@ -772,6 +768,60 @@ class TestWorkOrder(unittest.TestCase):
total_pl_qty
)
def test_job_card_scrap_item(self):
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
'Test RM Item 2 for Scrap Item Test']
company = '_Test Company with perpetual inventory'
for item_code in items:
create_item(item_code = item_code, is_stock_item = 1,
is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
item = 'Test FG Item for Scrap Item Test'
raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test']
if not frappe.db.get_value('BOM', {'item': item}):
bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
bom.with_operations = 1
bom.append('operations', {
'operation': '_Test Operation 1',
'workstation': '_Test Workstation 1',
'hour_rate': 20,
'time_in_mins': 60
})
bom.submit()
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
update_job_card(job_card)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.is_scrap_item:
self.assertEqual(row.qty, 1)
def update_job_card(job_card):
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
{
'item_code': 'Test RM Item 1 for Scrap Item Test',
'stock_qty': 2
},
{
'item_code': 'Test RM Item 2 for Scrap Item Test',
'stock_qty': 2
},
])
job_card_doc.append('time_logs', {
'from_time': now(),
'time_in_mins': 60,
'completed_qty': job_card_doc.for_quantity
})
job_card_doc.submit()
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
@@ -814,6 +864,7 @@ def make_wo_order_test_record(**args):
wo_order.get_items_and_operations_from_bom()
wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
if args.source_warehouse:
for item in wo_order.get("required_items"):

View File

@@ -306,8 +306,11 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.custom_fields_for_taxjar_integration
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_website_items
erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item

View File

@@ -7,6 +7,8 @@ def execute():
frappe.reload_doc("e_commerce", "doctype", "website_item")
frappe.reload_doc("e_commerce", "doctype", "website_item_tabbed_section")
frappe.reload_doc("e_commerce", "doctype", "website_offer")
frappe.reload_doc("e_commerce", "doctype", "recommended_items")
frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings")
frappe.reload_doc("stock", "doctype", "item")
item_fields = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image",

View File

@@ -0,0 +1,32 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.united_states.setup import add_permissions
def execute():
company = frappe.get_all('Company', filters = {'country': 'United States'}, fields=['name'])
if not company:
return
frappe.reload_doc("regional", "doctype", "product_tax_category")
custom_fields = {
'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1),
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
label='Taxable Amount', read_only=1)
],
'Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=True)

View File

@@ -0,0 +1,17 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
if frappe.db.table_exists('Supplier Item Group'):
frappe.reload_doc("selling", "doctype", "party_specific_item")
sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"])
for item in sig:
psi = frappe.new_doc("Party Specific Item")
psi.party_type = "Supplier"
psi.party = item.supplier
psi.restrict_based_on = "Item Group"
psi.based_on_value = item.item_group
psi.insert()

View File

@@ -0,0 +1,26 @@
# Copyright (c) 2021, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate')
if frappe.db.has_column('Tax Withholding Rate', 'fiscal_year'):
tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year'])
fiscal_year_map = {}
fiscal_year_details = frappe.get_all('Fiscal Year', fields=['name', 'year_start_date', 'year_end_date'])
for d in fiscal_year_details:
fiscal_year_map.setdefault(d.name, d)
for rate in tds_category_rates:
from_date = fiscal_year_map.get(rate.fiscal_year).get('year_start_date')
to_date = fiscal_year_map.get(rate.fiscal_year).get('year_end_date')
frappe.db.set_value('Tax Withholding Rate', rate.name, {
'from_date': from_date,
'to_date': to_date
})

View File

@@ -13,7 +13,7 @@ def execute():
frappe.reload_doc('stock', 'doctype', 'stock_settings')
def update_from_return_docs(doctype):
for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}):
for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1, 'return_against': ('!=', '')}):
# Update original receipt/delivery document from return
return_doc = frappe.get_cached_doc(doctype, return_doc.name)
try:

View File

@@ -14,12 +14,11 @@ from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document):
def on_submit(self):
if self.ref_doctype == "Employee Advance" and self.ref_docname:
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
self.update_return_amount_in_employee_advance()
self.update_employee_referral()
def on_cancel(self):
self.update_return_amount_in_employee_advance()
self.update_employee_referral(cancel=True)
def validate(self):
@@ -98,6 +97,17 @@ class AdditionalSalary(Document):
frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
frappe.bold("Accepted")))
def update_return_amount_in_employee_advance(self):
if self.ref_doctype == "Employee Advance" and self.ref_docname:
return_amount = frappe.db.get_value("Employee Advance", self.ref_docname, "return_amount")
if self.docstatus == 2:
return_amount -= self.amount
else:
return_amount += self.amount
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount)
def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral":
status = "Unpaid" if cancel else "Paid"

View File

@@ -4,18 +4,11 @@
frappe.ui.form.on('Salary Component', {
setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) {
let d = frappe.get_doc(cdt, cdn);
let root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
var d = locals[cdt][cdn];
return {
filters: {
"is_group": 0,
"company": d.company,
"root_type": root_type
"company": d.company
}
};
});

View File

@@ -487,7 +487,7 @@ class SalarySlip(TransactionBase):
self.calculate_component_amounts("deductions")
self.set_loan_repayment()
self.set_component_amounts_based_on_payment_days()
self.set_precision_for_component_amounts()
self.set_net_pay()
def set_net_pay(self):
@@ -713,6 +713,17 @@ class SalarySlip(TransactionBase):
component_row.amount = amount
self.update_component_amount_based_on_payment_days(component_row)
def update_component_amount_based_on_payment_days(self, component_row):
joining_date, relieving_date = self.get_joining_and_relieving_dates()
component_row.amount = self.get_amount_based_on_payment_days(component_row, joining_date, relieving_date)[0]
def set_precision_for_component_amounts(self):
for component_type in ("earnings", "deductions"):
for component_row in self.get(component_type):
component_row.amount = flt(component_row.amount, component_row.precision("amount"))
def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period):
if not payroll_period:
frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.")
@@ -870,14 +881,7 @@ class SalarySlip(TransactionBase):
return total_tax_paid
def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
if not relieving_date:
relieving_date = getdate(self.end_date)
if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
joining_date, relieving_date = self.get_joining_and_relieving_dates()
taxable_earnings = 0
additional_income = 0
@@ -888,7 +892,10 @@ class SalarySlip(TransactionBase):
if based_on_payment_days:
amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date)
else:
amount, additional_amount = earning.amount, earning.additional_amount
if earning.additional_amount:
amount, additional_amount = earning.amount, earning.additional_amount
else:
amount, additional_amount = earning.default_amount, earning.additional_amount
if earning.is_tax_applicable:
if additional_amount:
@@ -1059,7 +1066,7 @@ class SalarySlip(TransactionBase):
total += amount
return total
def set_component_amounts_based_on_payment_days(self):
def get_joining_and_relieving_dates(self):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
@@ -1069,9 +1076,7 @@ class SalarySlip(TransactionBase):
if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for component_type in ("earnings", "deductions"):
for d in self.get(component_type):
d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
return joining_date, relieving_date
def set_loan_repayment(self):
self.total_loan_repayment = 0

View File

@@ -17,6 +17,7 @@ from frappe.utils import (
getdate,
nowdate,
)
from frappe.utils.make_random import get_random
import erpnext
from erpnext.accounts.utils import get_fiscal_year
@@ -134,6 +135,65 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
create_salary_structure_assignment,
)
no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
# base = 50000
create_salary_structure_assignment(employee, salary_structure.name, company="_Test Company", currency="INR")
# mark employee absent for a day since this case works fine if payment days are equal to working days
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""", (month_start_date, month_end_date))[0][0]
mark_attendance(employee, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent
# make salary slip and assert payment days
ss = make_salary_slip_for_payment_days_dependency_test("test_payment_days_based_component@salary.com", salary_structure.name)
self.assertEqual(ss.absent_days, 1)
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1)
ss.reload()
payment_days_based_comp_amount = 0
for component in ss.earnings:
if component.salary_component == "HRA - Payment Days":
payment_days_based_comp_amount = flt(component.amount, component.precision("amount"))
break
# check if the dependent component is calculated using the amount updated after payment days
actual_amount = 0
precision = 0
for component in ss.deductions:
if component.salary_component == "P - Employee Provident Fund":
precision = component.precision("amount")
actual_amount = flt(component.amount, precision)
break
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
self.assertEqual(actual_amount, expected_amount)
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
def test_salary_slip_with_holidays_included(self):
no_of_days = self.get_no_of_days()
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
@@ -851,6 +911,7 @@ def setup_test():
def make_holiday_list():
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List")
if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"):
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
@@ -861,3 +922,94 @@ def make_holiday_list():
}).insert()
holiday_list.get_weekly_off_dates()
holiday_list.save()
holiday_list = holiday_list.name
return holiday_list
def make_salary_structure_for_payment_days_based_component_dependency():
earnings = [
{
"salary_component": "Basic Salary - Payment Days",
"abbr": "P_BS",
"type": "Earning",
"formula": "base",
"amount_based_on_formula": 1
},
{
"salary_component": "HRA - Payment Days",
"abbr": "P_HRA",
"type": "Earning",
"depends_on_payment_days": 1,
"amount_based_on_formula": 1,
"formula": "base * 0.20"
}
]
make_salary_component(earnings, False, company_list=["_Test Company"])
deductions = [
{
"salary_component": "P - Professional Tax",
"abbr": "P_PT",
"type": "Deduction",
"depends_on_payment_days": 1,
"amount": 200.00
},
{
"salary_component": "P - Employee Provident Fund",
"abbr": "P_EPF",
"type": "Deduction",
"exempted_from_income_tax": 1,
"amount_based_on_formula": 1,
"depends_on_payment_days": 0,
"formula": "(gross_pay - P_HRA) * 0.12"
}
]
make_salary_component(deductions, False, company_list=["_Test Company"])
salary_structure = "Salary Structure with PF"
if frappe.db.exists("Salary Structure", salary_structure):
frappe.db.delete("Salary Structure", salary_structure)
details = {
"doctype": "Salary Structure",
"name": salary_structure,
"company": "_Test Company",
"payroll_frequency": "Monthly",
"payment_account": get_random("Account", filters={"account_currency": "INR"}),
"currency": "INR"
}
salary_structure_doc = frappe.get_doc(details)
for entry in earnings:
salary_structure_doc.append("earnings", entry)
for entry in deductions:
salary_structure_doc.append("deductions", entry)
salary_structure_doc.insert()
salary_structure_doc.submit()
return salary_structure_doc
def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure):
employee = frappe.db.get_value("Employee", {
"user_id": employee
},
["name", "company", "employee_name"],
as_dict=True)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})})
if not salary_slip_name:
salary_slip = make_salary_slip(salary_structure, employee=employee.name)
salary_slip.employee_name = employee.employee_name
salary_slip.payroll_frequency = "Monthly"
salary_slip.posting_date = nowdate()
salary_slip.insert()
else:
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
return salary_slip

View File

@@ -227,7 +227,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
{
fieldtype: "HTML",
fieldname: "no_matching_vouchers",
options: "<div class='text-muted text-center'>No Matching Vouchers Found</div>"
options: "<div class=\"text-muted text-center\">No Matching Vouchers Found</div>"
},
{
fieldtype: "Section Break",

View File

@@ -105,6 +105,8 @@ $.extend(shopping_cart, {
},
set_cart_count: function(animate=false) {
$(".intermediate-empty-cart").remove();
var cart_count = frappe.get_cookie("cart_count");
if(frappe.session.user==="Guest") {
cart_count = 0;
@@ -119,13 +121,20 @@ $.extend(shopping_cart, {
if(parseInt(cart_count) === 0 || cart_count === undefined) {
$cart.css("display", "none");
$(".cart-items").html('Cart is Empty');
$(".cart-tax-items").hide();
$(".btn-place-order").hide();
$(".cart-payment-addresses").hide();
let intermediate_empty_cart_msg = `
<div class="text-center w-100 intermediate-empty-cart mt-4 mb-4 text-muted">
${ __("Cart is Empty") }
</div>
`;
$(".cart-table").after(intermediate_empty_cart_msg);
}
else {
$cart.css("display", "inline");
$("#cart-count").text(cart_count);
}
if(cart_count) {
@@ -152,7 +161,10 @@ $.extend(shopping_cart, {
callback: function(r) {
if(!r.exc) {
$(".cart-items").html(r.message.items);
$(".cart-tax-items").html(r.message.taxes);
$(".cart-tax-items").html(r.message.total);
$(".payment-summary").html(r.message.taxes_and_totals);
shopping_cart.set_cart_count();
if (cart_dropdown != true) {
$(".cart-icon").hide();
}

View File

@@ -709,6 +709,9 @@ erpnext.utils.map_current_doc = function(opts) {
setters: opts.setters,
get_query: opts.get_query,
add_filters_group: 1,
allow_child_item_selection: opts.allow_child_item_selection,
child_fieldname: opts.child_fielname,
child_columns: opts.child_columns,
action: function(selections, args) {
let values = selections;
if(values.length === 0){
@@ -716,7 +719,7 @@ erpnext.utils.map_current_doc = function(opts) {
return;
}
opts.source_name = values;
opts.setters = args;
opts.args = args;
d.dialog.hide();
_map();
},

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Product Tax Category', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "field:product_tax_code",
"creation": "2021-08-23 12:33:37.910225",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"product_tax_code",
"column_break_2",
"category_name",
"section_break_4",
"description"
],
"fields": [
{
"fieldname": "product_tax_code",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Product Tax Code",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "category_name",
"fieldtype": "Data",
"label": "Category Name",
"length": 255
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-24 09:10:25.313642",
"modified_by": "Administrator",
"module": "Regional",
"name": "Product Tax Category",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "category_name",
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProductTaxCategory(Document):
pass

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestSupplierItemGroup(unittest.TestCase):
class TestProductTaxCategory(unittest.TestCase):
pass

View File

@@ -803,11 +803,11 @@ def set_tax_withholding_category(company):
accounts = [dict(company=company, account=tds_account)]
try:
fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0]
fiscal_year_details = get_fiscal_year(today(), verbose=0, company=company)
except FiscalYearError:
pass
docs = get_tds_details(accounts, fiscal_year)
docs = get_tds_details(accounts, fiscal_year_details)
for d in docs:
if not frappe.db.exists("Tax Withholding Category", d.get("name")):
@@ -822,9 +822,10 @@ def set_tax_withholding_category(company):
if accounts:
doc.append("accounts", accounts[0])
if fiscal_year:
if fiscal_year_details:
# if fiscal year don't match with any of the already entered data, append rate row
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
fy_exist = [k for k in doc.get('rates') if k.get('from_date') <= fiscal_year_details[1] \
and k.get('to_date') >= fiscal_year_details[2]]
if not fy_exist:
doc.append("rates", d.get('rates')[0])
@@ -847,149 +848,149 @@ def set_tds_account(docs, company):
}
])
def get_tds_details(accounts, fiscal_year):
def get_tds_details(accounts, fiscal_year_details):
# bootstrap default tax withholding sections
return [
dict(name="TDS - 194C - Company",
category_name="Payment to Contractors (Single / Aggregate)",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
"single_threshold": 30000, "cumulative_threshold": 100000}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]),
dict(name="TDS - 194C - Individual",
category_name="Payment to Contractors (Single / Aggregate)",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
"single_threshold": 30000, "cumulative_threshold": 100000}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]),
dict(name="TDS - 194C - No PAN / Invalid PAN",
category_name="Payment to Contractors (Single / Aggregate)",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 30000, "cumulative_threshold": 100000}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]),
dict(name="TDS - 194D - Company",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194D - Company Assessee",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194D - Individual",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194D - No PAN / Invalid PAN",
category_name="Insurance Commission",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194DA - Company",
category_name="Non-exempt payments made under a life insurance policy",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
"single_threshold": 100000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]),
dict(name="TDS - 194DA - Individual",
category_name="Non-exempt payments made under a life insurance policy",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1,
"single_threshold": 100000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]),
dict(name="TDS - 194DA - No PAN / Invalid PAN",
category_name="Non-exempt payments made under a life insurance policy",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 100000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]),
dict(name="TDS - 194H - Company",
category_name="Commission / Brokerage",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194H - Individual",
category_name="Commission / Brokerage",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194H - No PAN / Invalid PAN",
category_name="Commission / Brokerage",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 15000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent - Company",
category_name="Rent",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent - Individual",
category_name="Rent",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent - No PAN / Invalid PAN",
category_name="Rent",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent/Machinery - Company",
category_name="Rent-Plant / Machinery",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent/Machinery - Individual",
category_name="Rent-Plant / Machinery",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN",
category_name="Rent-Plant / Machinery",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 180000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Professional Fees - Company",
category_name="Professional Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 30000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Professional Fees - Individual",
category_name="Professional Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 30000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN",
category_name="Professional Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 30000, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Director Fees - Company",
category_name="Director Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 0, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Director Fees - Individual",
category_name="Director Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 0, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]),
dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN",
category_name="Director Fees",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 0, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]),
dict(name="TDS - 194 - Dividends - Company",
category_name="Dividends",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 2500, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]),
dict(name="TDS - 194 - Dividends - Individual",
category_name="Dividends",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10,
"single_threshold": 2500, "cumulative_threshold": 0}]),
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]),
dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN",
category_name="Dividends",
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}])
rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2],
"tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}])
]
def create_gratuity_rule():

View File

@@ -96,35 +96,36 @@ class Gstr1Report(object):
def get_b2c_data(self):
b2cs_output = {}
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
for rate, items in items_based_on_rate.items():
place_of_supply = invoice_details.get("place_of_supply")
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
if self.invoices:
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
for rate, items in items_based_on_rate.items():
place_of_supply = invoice_details.get("place_of_supply")
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{
"place_of_supply": "",
"ecommerce_gstin": "",
"rate": "",
"taxable_value": 0,
"cess_amount": 0,
"type": "",
"invoice_number": invoice_details.get("invoice_number"),
"posting_date": invoice_details.get("posting_date"),
"invoice_value": invoice_details.get("base_grand_total"),
})
b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin), {
"place_of_supply": "",
"ecommerce_gstin": "",
"rate": "",
"taxable_value": 0,
"cess_amount": 0,
"type": "",
"invoice_number": invoice_details.get("invoice_number"),
"posting_date": invoice_details.get("posting_date"),
"invoice_value": invoice_details.get("base_grand_total"),
})
row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
row["place_of_supply"] = place_of_supply
row["ecommerce_gstin"] = ecommerce_gstin
row["rate"] = rate
row["taxable_value"] += sum([abs(net_amount)
for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items])
row["cess_amount"] += flt(self.invoice_cess.get(inv), 2)
row["type"] = "E" if ecommerce_gstin else "OE"
row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
row["place_of_supply"] = place_of_supply
row["ecommerce_gstin"] = ecommerce_gstin
row["rate"] = rate
row["taxable_value"] += sum([abs(net_amount)
for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items])
row["cess_amount"] += flt(self.invoice_cess.get(inv), 2)
row["type"] = "E" if ecommerce_gstin else "OE"
for key, value in iteritems(b2cs_output):
self.data.append(value)
for key, value in iteritems(b2cs_output):
self.data.append(value)
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
row = []
@@ -173,9 +174,10 @@ class Gstr1Report(object):
company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True)
self.filters.update({
'company_gstins': company_gstins
})
if company_gstins:
self.filters.update({
'company_gstins': company_gstins
})
invoice_data = frappe.db.sql("""
select
@@ -212,7 +214,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B":
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1"
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@@ -221,7 +223,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2C Large":
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "B2C Small":
conditions += """ AND (
@@ -234,8 +236,8 @@ class Gstr1Report(object):
elif self.filters.get("type_of_business") == "CDNR-UNREG":
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND ABS(grand_total) > {0} AND (is_return = 1 OR is_debit_note = 1)
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""".format(flt(b2c_limit))
AND (is_return = 1 OR is_debit_note = 1)
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')"""
elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
@@ -1050,6 +1052,7 @@ def get_company_gstin_number(company, address=None, all_gstins=False):
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
["Dynamic Link", "parenttype", "=", "Address"],
["gstin", "!=", '']
]
gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc")
if gstin and not all_gstins:

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,42 @@
from __future__ import unicode_literals
import frappe
import os
import json
from frappe.permissions import add_permission, update_permission_property
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup(company=None, patch=True):
# Company independent fixtures should be called only once at the first company setup
if frappe.db.count('Company', {'country': 'United States'}) <=1:
setup_company_independent_fixtures(patch=patch)
def setup_company_independent_fixtures(company=None, patch=True):
add_product_tax_categories()
make_custom_fields()
add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
add_print_formats()
# Product Tax categories imported from taxjar api
def add_product_tax_categories():
with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
tax_categories = json.loads(f.read())
create_tax_categories(tax_categories['categories'])
def create_tax_categories(data):
for d in data:
tax_category = frappe.new_doc('Product Tax Category')
tax_category.description = d.get("description")
tax_category.product_tax_code = d.get("product_tax_code")
tax_category.category_name = d.get("name")
try:
tax_category.db_insert()
except frappe.DuplicateEntryError:
pass
def make_custom_fields(update=True):
custom_fields = {
'Supplier': [
@@ -32,10 +61,29 @@ def make_custom_fields(update=True):
'Quotation': [
dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges',
label='Is customer exempted from sales tax?')
],
'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1),
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
label='Taxable Amount', read_only=1)
],
'Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=update)
def add_permissions():
doctype = "Product Tax Category"
for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1)
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)

View File

@@ -510,8 +510,14 @@
"idx": 363,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-25 18:56:09.929905",
"links": [
{
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-09-06 17:38:54.196663",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -14,7 +14,7 @@ from frappe.contacts.address_and_contact import (
)
from frappe.desk.reportview import build_match_conditions, get_filters_cond
from frappe.model.mapper import get_mapped_doc
from frappe.model.naming import set_name_by_naming_series
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.rename_doc import update_linked_doctypes
from frappe.utils import cint, cstr, flt, get_formatted_email, today
from frappe.utils.user import get_users_with_role
@@ -40,8 +40,10 @@ class Customer(TransactionBase):
cust_master_name = frappe.defaults.get_global_default('cust_master_name')
if cust_master_name == 'Customer Name':
self.name = self.get_customer_name()
else:
elif cust_master_name == 'Naming Series':
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def get_customer_name(self):

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Supplier Item Group', {
frappe.ui.form.on('Party Specific Item', {
// refresh: function(frm) {
// }

View File

@@ -0,0 +1,77 @@
{
"actions": [],
"creation": "2021-08-27 19:28:07.559978",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"party_type",
"party",
"column_break_3",
"restrict_based_on",
"based_on_value"
],
"fields": [
{
"fieldname": "party_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Party Type",
"options": "Customer\nSupplier",
"reqd": 1
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party Name",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "restrict_based_on",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Restrict Items Based On",
"options": "Item\nItem Group\nBrand",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "based_on_value",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Based On Value",
"options": "restrict_based_on",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-14 13:27:58.612334",
"modified_by": "Administrator",
"module": "Selling",
"name": "Party Specific Item",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "party",
"track_changes": 1
}

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class PartySpecificItem(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Party Specific Item',
'party_type': self.party_type,
'party': self.party,
'restrict_based_on': self.restrict_based_on,
'based_on': self.based_on_value,
})
if exists:
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))

View File

@@ -0,0 +1,38 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from erpnext.controllers.queries import item_query
test_dependencies = ['Item', 'Customer', 'Supplier']
def create_party_specific_item(**args):
psi = frappe.new_doc("Party Specific Item")
psi.party_type = args.get('party_type')
psi.party = args.get('party')
psi.restrict_based_on = args.get('restrict_based_on')
psi.based_on_value = args.get('based_on_value')
psi.insert()
class TestPartySpecificItem(unittest.TestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")
self.item = frappe.get_last_doc("Item")
def test_item_query_for_customer(self):
create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name)
filters = {'is_sales_item': 1, 'customer': self.customer.name}
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
for item in items:
self.assertEqual(item[0], self.item.name)
def test_item_query_for_supplier(self):
create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group)
filters = {'supplier': self.supplier.name, 'is_purchase_item': 1}
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
for item in items:
self.assertEqual(item[2], self.item.item_group)

View File

@@ -41,14 +41,14 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Customer Naming By",
"options": "Customer Name\nNaming Series"
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series"
"options": "Campaign Name\nNaming Series\nAuto Name"
},
{
"fieldname": "customer_group",

View File

@@ -297,6 +297,7 @@ erpnext.PointOfSale.Payment = class {
this.render_payment_mode_dom();
this.make_invoice_fields_control();
this.update_totals_section();
this.focus_on_default_mop();
}
edit_cart() {
@@ -378,17 +379,24 @@ erpnext.PointOfSale.Payment = class {
});
this[`${mode}_control`].toggle_label(false);
this[`${mode}_control`].set_value(p.amount);
});
this.render_loyalty_points_payment_mode();
this.attach_cash_shortcuts(doc);
}
focus_on_default_mop() {
const doc = this.events.get_frm().doc;
const payments = doc.payments;
payments.forEach(p => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
if (p.default) {
setTimeout(() => {
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
}, 500);
}
});
this.render_loyalty_points_payment_mode();
this.attach_cash_shortcuts(doc);
}
attach_cash_shortcuts(doc) {

View File

@@ -63,7 +63,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
this.frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1}
filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
}
});
}
@@ -243,7 +243,12 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
if(df && editable_price_list_rate) {
df.read_only = 0;
const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item");
if (!this.frm.fields_dict[parent_field]) return;
this.frm.fields_dict[parent_field].grid.update_docfield_property(
'price_list_rate', 'read_only', 0
);
}
},

View File

@@ -950,7 +950,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
"modified": "2021-08-26 12:23:07.277077",
"modified": "2021-09-10 12:23:07.277077",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -2,19 +2,32 @@
// For license information, please see license.txt
frappe.ui.form.on('Item Variant Settings', {
setup: function(frm) {
refresh: function(frm) {
const allow_fields = [];
const exclude_fields = ["naming_series", "item_code", "item_name", "published_in_website",
"opening_stock", "variant_of", "valuation_rate"];
const existing_fields = frm.doc.fields.map(row => row.field_name);
const exclude_fields = [...existing_fields, "naming_series", "item_code", "item_name",
"show_in_website", "show_variant_in_website", "standard_rate", "opening_stock", "image",
"variant_of", "valuation_rate", "barcodes", "website_image", "thumbnail",
"website_specifiations", "web_long_description", "has_variants", "attributes"];
const exclude_field_types = ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'];
frappe.model.with_doctype('Item', () => {
frappe.get_meta('Item').fields.forEach(d => {
if(!in_list(['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only'], d.fieldtype)
if (!in_list(exclude_field_types, d.fieldtype)
&& !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
allow_fields.push(d.fieldname);
}
});
if (allow_fields.length == 0) {
allow_fields.push({
label: __("No additional fields available"),
value: "",
});
}
frm.fields_dict.fields.grid.update_docfield_property(
'field_name', 'options', allow_fields
);

View File

@@ -6,10 +6,13 @@
from __future__ import unicode_literals
import json
import frappe
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
from six import string_types
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
@@ -269,7 +272,10 @@ def update_status(name, status):
material_request.update_status(status)
@frappe.whitelist()
def make_purchase_order(source_name, target_doc=None):
def make_purchase_order(source_name, target_doc=None, args={}):
if isinstance(args, string_types):
args = json.loads(args)
def postprocess(source, target_doc):
if frappe.flags.args and frappe.flags.args.default_supplier:
@@ -284,7 +290,10 @@ def make_purchase_order(source_name, target_doc=None):
set_missing_values(source, target_doc)
def select_item(d):
return d.ordered_qty < d.stock_qty
filtered_items = args.get('filtered_children', [])
child_filter = d.name in filtered_items if filtered_items else True
return d.ordered_qty < d.stock_qty and child_filter
doclist = get_mapped_doc("Material Request", source_name, {
"Material Request": {

View File

@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import json
from collections import defaultdict
import frappe
from frappe import _
@@ -684,7 +685,7 @@ class StockEntry(StockController):
def validate_bom(self):
for d in self.get('items'):
if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse):
if d.bom_no and d.is_finished_item:
item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no)
@@ -1191,13 +1192,88 @@ class StockEntry(StockController):
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
fetch_exploded = 0, fetch_scrap_items = 1)
fetch_exploded = 0, fetch_scrap_items = 1) or {}
for item in itervalues(item_dict):
item.from_warehouse = ""
item.is_scrap_item = 1
for row in self.get_scrap_items_from_job_card():
if row.stock_qty <= 0:
continue
item_row = item_dict.get(row.item_code)
if not item_row:
item_row = frappe._dict({})
item_row.update({
'uom': row.stock_uom,
'from_warehouse': '',
'qty': row.stock_qty + flt(item_row.stock_qty),
'converison_factor': 1,
'is_scrap_item': 1,
'item_name': row.item_name,
'description': row.description,
'allow_zero_valuation_rate': 1
})
item_dict[row.item_code] = item_row
return item_dict
def get_scrap_items_from_job_card(self):
if not self.pro_doc:
self.set_work_order_details()
scrap_items = frappe.db.sql('''
SELECT
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
FROM
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
WHERE
JCSI.parent = JC.name AND JC.docstatus = 1
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
GROUP BY
JCSI.item_code
''', self.work_order, as_dict=1)
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
if pending_qty <=0:
return []
used_scrap_items = self.get_used_scrap_items()
for row in scrap_items:
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
if used_scrap_items.get(row.item_code):
used_scrap_items[row.item_code] -= row.stock_qty
if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
row.stock_qty = frappe.utils.ceil(row.stock_qty)
return scrap_items
def get_used_scrap_items(self):
used_scrap_items = defaultdict(float)
data = frappe.get_all(
'Stock Entry',
fields = [
'`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
],
filters = [
['Stock Entry', 'work_order', '=', self.work_order],
['Stock Entry Detail', 'is_scrap_item', '=', 1],
['Stock Entry', 'docstatus', '=', 1],
['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
]
)
for row in data:
used_scrap_items[row.item_code] += row.qty
return used_scrap_items
def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item',
@@ -1264,9 +1340,9 @@ class StockEntry(StockController):
po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
`tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
manufacturing_qty = flt(po_qty.qty)
manufacturing_qty = flt(po_qty.qty) or 1
produced_qty = flt(po_qty.produced_qty)
trans_qty = flt(po_qty.material_transferred_for_manufacturing)
trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1
for item in transferred_materials:
qty= item.qty
@@ -1417,8 +1493,8 @@ class StockEntry(StockController):
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name", "serial_no", "batch_no"]:
for field in ["idx", "po_detail", "original_item", "expense_account",
"description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field))

View File

@@ -592,6 +592,11 @@ def get_stock_balance_for(item_code, warehouse,
item_dict = frappe.db.get_value("Item", item_code,
["has_serial_no", "has_batch_no"], as_dict=1)
if not item_dict:
# In cases of data upload to Items table
msg = _("Item {} does not exist.").format(item_code)
frappe.throw(msg, title=_("Missing"))
serial_nos = ""
with_serial_no = True if item_dict.get("has_serial_no") else False
data = get_stock_balance(item_code, warehouse, posting_date, posting_time,

View File

@@ -22,7 +22,15 @@ frappe.query_reports["Stock Ageing"] = {
"fieldname":"warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse"
"options": "Warehouse",
get_query: () => {
const company = frappe.query_report.get_filter_value("company");
return {
filters: {
...company && {company},
}
};
}
},
{
"fieldname":"item_code",

View File

@@ -53,13 +53,14 @@ frappe.query_reports["Stock Balance"] = {
"width": "80",
"options": "Warehouse",
get_query: () => {
var warehouse_type = frappe.query_report.get_filter_value('warehouse_type');
if(warehouse_type){
return {
filters: {
'warehouse_type': warehouse_type
}
};
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company");
return {
filters: {
...warehouse_type && {warehouse_type},
...company && {company}
}
}
}
},

View File

@@ -0,0 +1,63 @@
import unittest
from typing import List, Tuple
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
}
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}),
("Stock Balance", {"_optional": True}),
("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}),
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),
("COGS By Item Group", {}),
("Stock Qty vs Serial No Count", {"warehouse": "_Test Warehouse - _TC"}),
(
"Stock and Account Value Comparison",
{
"company": "_Test Company with perpetual inventory",
"account": "Stock In Hand - TCP1",
"as_on_date": "2021-01-01",
},
),
("Product Bundle Balance", {"date": "2022-01-01", "_optional": True}),
(
"Stock Analytics",
{
"from_date": "2021-01-01",
"to_date": "2021-12-31",
"value_quantity": "Quantity",
"_optional": True,
},
),
("Warehouse wise Item Balance Age and Value", {"_optional": True}),
("Item Variant Details", {"item": "_Test Variant Item",}),
("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
]
OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
class TestReports(unittest.TestCase):
def test_execute_all_stock_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report(
report_name=report,
module="Stock",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@@ -399,7 +399,8 @@ class update_entries_after(object):
return
# Get dynamic incoming/outgoing rate
self.get_dynamic_incoming_outgoing_rate(sle)
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
if sle.serial_no:
self.get_serialized_values(sle)
@@ -439,7 +440,8 @@ class update_entries_after(object):
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
self.update_outgoing_rate_on_transaction(sle)
if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle)
def validate_negative_stock(self, sle):
"""
@@ -673,11 +675,15 @@ class update_entries_after(object):
if self.wh_data.stock_queue[-1][1]==incoming_rate:
self.wh_data.stock_queue[-1][0] += actual_qty
else:
# Item has a positive balance qty, add new entry
if self.wh_data.stock_queue[-1][0] > 0:
self.wh_data.stock_queue.append([actual_qty, incoming_rate])
else:
else: # negative balance qty
qty = self.wh_data.stock_queue[-1][0] + actual_qty
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
if qty > 0: # new balance qty is positive
self.wh_data.stock_queue[-1] = [qty, incoming_rate]
else: # new balance qty is still negative, maintain same rate
self.wh_data.stock_queue[-1][0] = qty
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:

View File

@@ -57,7 +57,7 @@ $.extend(shopping_cart, {
callback: function(r) {
d.hide();
if (!r.exc) {
$(".cart-tax-items").html(r.message.taxes);
$(".cart-tax-items").html(r.message.total);
shopping_cart.parent.find(
`.address-container[data-address-type="${address_type}"]`
).html(r.message.address);
@@ -214,12 +214,15 @@ $.extend(shopping_cart, {
},
place_order: function(btn) {
shopping_cart.freeze();
return frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.place_order",
btn: btn,
callback: function(r) {
if(r.exc) {
shopping_cart.unfreeze();
var msg = "";
if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>");
@@ -230,7 +233,6 @@ $.extend(shopping_cart, {
.html(msg || frappe._("Something went wrong!"))
.toggle(true);
} else {
$('.cart-container table').hide();
$(btn).hide();
window.location.href = '/orders/' + encodeURIComponent(r.message);
}
@@ -239,12 +241,15 @@ $.extend(shopping_cart, {
},
request_quotation: function(btn) {
shopping_cart.freeze();
return frappe.call({
type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
btn: btn,
callback: function(r) {
if(r.exc) {
shopping_cart.unfreeze();
var msg = "";
if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>");
@@ -255,7 +260,6 @@ $.extend(shopping_cart, {
.html(msg || frappe._("Something went wrong!"))
.toggle(true);
} else {
$('.cart-container table').hide();
$(btn).hide();
window.location.href = '/quotations/' + encodeURIComponent(r.message);
}

View File

@@ -0,0 +1,10 @@
<!-- Total at the end of the cart items -->
<tr>
<th></th>
<th class="text-left item-grand-total" colspan="1">
{{ _("Total") }}
</th>
<th class="text-left item-grand-total totals" colspan="3">
{{ doc.get_formatted("total") }}
</th>
</tr>

View File

@@ -1,62 +1,61 @@
<!-- Payment -->
<div class="mb-3 frappe-card p-5 payment-summary">
<h6>
{{ _("Payment Summary") }}
</h6>
<div class="card h-100">
<div class="card-body p-0">
<table class="table w-100">
<tr>
<td class="bill-label">{{ _("Net Total (") + frappe.utils.cstr(doc.items|len) + _(" Items)") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
</tr>
<h6>
{{ _("Payment Summary") }}
</h6>
<div class="card h-100">
<div class="card-body p-0">
<table class="table w-100">
<tr>
{% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}
<td class="bill-label">{{ _("Net Total (") + total_items + _(" Items)") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
</tr>
<!-- taxes -->
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
<tr>
<td class="bill-label">
{{ d.description }}
</td>
<td class="bill-content text-right">
{{ d.get_formatted("base_tax_amount") }}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
<!-- taxes -->
{% for d in doc.taxes %}
{% if d.base_tax_amount %}
<tr>
<td class="bill-label">
{{ d.description }}
</td>
<td class="bill-content text-right">
{{ d.get_formatted("base_tax_amount") }}
</td>
</tr>
{% endif %}
{% endfor %}
</table>
<!-- TODO: Apply Coupon Dialog-->
<!-- {% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}
{% if show_coupon_code %}
<button class="btn btn-coupon-code w-100 text-left">
<svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--gray-600)" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="ml-2">Apply Coupon</span>
</button>
{% endif %} -->
<!-- TODO: Apply Coupon Dialog-->
<!-- {% set show_coupon_code = cart_settings.show_apply_coupon_code_in_website and cart_settings.enable_checkout %}
{% if show_coupon_code %}
<button class="btn btn-coupon-code w-100 text-left">
<svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--gray-600)" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="ml-2">Apply Coupon</span>
</button>
{% endif %} -->
<table class="table w-100 grand-total mt-6">
<tr>
<td class="bill-content net-total">{{ _("Grand Total") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("grand_total") }}</td>
</tr>
</table>
<table class="table w-100 grand-total mt-6">
<tr>
<td class="bill-content net-total">{{ _("Grand Total") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("grand_total") }}</td>
</tr>
</table>
{% if cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order font-md w-100" type="button">
{{ _('Place Order') }}
</button>
{% else %}
<button class="btn btn-primary btn-request-for-quotation font-md w-100" type="button">
{{ _('Request for Quote') }}
</button>
{% endif %}
</div>
{% if cart_settings.enable_checkout %}
<button class="btn btn-primary btn-place-order font-md w-100" type="button">
{{ _('Place Order') }}
</button>
{% else %}
<button class="btn btn-primary btn-request-for-quotation font-md w-100" type="button">
{{ _('Request for Quote') }}
</button>
{% endif %}
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More